Crablog Initial Commit

This commit is contained in:
Andrey Tkachenko 2023-10-13 18:11:23 +04:00
parent 9ca9b36666
commit 0e8316588b
32 changed files with 2745 additions and 2585 deletions

View File

@ -16,6 +16,6 @@ jobs:
only: only:
- master - master
script: script:
- docker build -t kilerd/rubble:latest . - docker build -t andreytkachenko/crablog:latest .
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
- docker push kilerd/rubble:latest - docker push andreytkachenko/crablog:latest

3158
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,47 @@
[package] [package]
name = "rubble" name = "crablog"
description = "a slight blog system" description = "a slight blog system"
version = "0.3.3" version = "0.4.0"
authors = ["Kilerd Chan <blove694@gmail.com>"] authors = ["Andrey Tkachenko <andrey@aidev.ru>", "Kilerd Chan <blove694@gmail.com>"]
license = "MIT" license = "MIT"
edition = "2018" edition = "2021"
build = "build.rs"
[dependencies] [dependencies]
dotenv = "0.15.0" dotenv = "0.15.0"
openssl = "0.10.27" openssl = "0.10.27"
# serde # serde
serde = "1.0.104" serde = "1.0"
serde_derive = "1.0.104" serde_derive = "1.0"
r2d2 = "0.8.8" diesel = { version = "2.1.3", features = ["postgres", "postgres_backend", "chrono"] }
diesel = { version = "1.4.3", features = ["postgres", "r2d2", "chrono"] } diesel_derives = "2.1.2"
diesel_derives = "1.4.1" diesel_migrations = "2.1.0"
diesel_migrations = "1.4.0"
tera = "1.0.2" tera = "1.19.1"
pulldown-cmark = { version = "0.6.1", default-features = false } pulldown-cmark = { version = "0.9.3", default-features = false }
chrono = { version = "0.4.10", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
rust-crypto = "0.2.36" rust-crypto = "0.2.36"
http = "0.2.0" http = "0.2.9"
actix-web= "2.0.0" futures = "0.3.28"
actix-files = "0.2.1"
futures = "0.3.1"
# log # log
log = "0.4.8" log = "0.4.20"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.5.0"
time = "0.1" time = "0.3.29"
rand = "0.7.3" rand = "0.8.5"
rss = "1.9.0" rss = "1.9.0"
actix = "0.9.0"
jsonwebtoken = "7.0.1"
actix-cors = "0.2.0"
actix-rt = "1.0.0"
once_cell = "1.3.1"
derive_more = "0.99.2" derive_more = "0.99.2"
once_cell = "1.18.0"
jsonwebtoken = "8.3.0"
diesel-async = { version = "0.4.1", git = "https://github.com/weiznich/diesel_async.git", features = ["deadpool", "tokio-postgres", "postgres", "async-connection-wrapper"] }
axum = { version = "0.6.20", features = ["tokio"] }
tokio = { version = "1.33.0", features = ["rt", "macros"] }
anyhow = { version = "1.0.75", features = ["backtrace"] }
tower-http = { version = "0.4.4", features = ["fs", "cors", "normalize-path", "tokio", "trace", "auth"] }
thiserror = "1.0"

View File

@ -2,8 +2,8 @@ FROM clux/muslrust:stable as builder
WORKDIR /app WORKDIR /app
RUN USER=root cargo new rubble RUN USER=root cargo new crablog
WORKDIR /app/rubble WORKDIR /app/crablog
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
@ -11,25 +11,25 @@ RUN echo 'fn main() { println!("Dummy") }' > ./src/main.rs
RUN cargo build --release RUN cargo build --release
RUN rm -r target/x86_64-unknown-linux-musl/release/.fingerprint/rubble-* RUN rm -r target/x86_64-unknown-linux-musl/release/.fingerprint/crablog-*
COPY src src/ COPY src src/
COPY migrations migrations/ COPY migrations migrations/
COPY templates templates/ COPY templates templates/
RUN cargo build --release --frozen --bin rubble RUN cargo build --release --frozen --bin crablog
FROM alpine:latest FROM alpine:latest
COPY --from=builder /app/rubble/migrations /application/migrations COPY --from=builder /app/crablog/migrations /application/migrations
COPY --from=builder /app/rubble/templates /application/templates COPY --from=builder /app/crablog/templates /application/templates
COPY --from=builder /app/rubble/target/x86_64-unknown-linux-musl/release/rubble /application/rubble COPY --from=builder /app/crablog/target/x86_64-unknown-linux-musl/release/crablog /application/crablog
EXPOSE 8000 EXPOSE 8000
ENV DATABASE_URL postgres://root@postgres/rubble ENV DATABASE_URL postgres://root@postgres/crablog
WORKDIR /application WORKDIR /application
CMD ["./rubble"] CMD ["./crablog"]

View File

@ -1,6 +1,6 @@
<img align="right" width="128" height="128" src="/rubble.png"> <img align="right" width="128" height="128" src="/crablog.png">
# Rubble # Crablob
a lightweight blog engine written by Rust. a lightweight blog engine written by Rust.
@ -18,7 +18,7 @@ Cause this project is also the tentative staff I try to write something in Rust,
## Template ## Template
Project rubble highly depends on tera, a fast and effective template engine in Rust, which means that you can write your own template with tera syntax. Project crablog highly depends on tera, a fast and effective template engine in Rust, which means that you can write your own template with tera syntax.
There are files in template folder as follow, which are the template for each page: There are files in template folder as follow, which are the template for each page:
@ -31,7 +31,7 @@ There are files in template folder as follow, which are the template for each pa
Obviously you can learn how to write this template by the guide of official template folder, and how to use tera syntax in tera's official website. Obviously you can learn how to write this template by the guide of official template folder, and how to use tera syntax in tera's official website.
## How to use it ## How to use it
After deploying rubble to your host, the first thing you need to do is login to the admin panel with url `http://yourdomain.com/admin`. And the default admin user and password is as follow: After deploying crablog to your host, the first thing you need to do is login to the admin panel with url `http://yourdomain.com/admin`. And the default admin user and password is as follow:
- Username: `admin` - Username: `admin`
- Password: `password` - Password: `password`
@ -39,26 +39,26 @@ after logging in, please modify the default password of admin. Then you can enjo
## Deploy using Docker ## Deploy using Docker
you can easily use Docker to create your own rubble application. And the latest version of it and each tagged version would be built as docker images storing in Docker Hub automatically. So you can easily pull those images by using `docker pull kilerd/rubble:latest` you can easily use Docker to create your own crablog application. And the latest version of it and each tagged version would be built as docker images storing in Docker Hub automatically. So you can easily pull those images by using `docker pull andreytkachenko/crablog:latest`
Rubble uses PostgresQL as data storage, so before strating rubble application, you need to start your postgres service and link it to rubble. Crablog uses PostgresQL as data storage, so before strating crablog application, you need to start your postgres service and link it to crablob.
Rubble image can accept some environment variable for setting up: Crablog image can accept some environment variable for setting up:
- `DATABASE_URL` url of postgresQL - `DATABASE_URL` url of postgresQL
### Docker Stack ### Docker Stack
But we recommend to deploy rubble with Docker Swarm or Kubenetes. here is a simple file to create a whole rubble application with postgresQL`docker-compose.yml` : But we recommend to deploy crablog with Docker Swarm or Kubenetes. here is a simple file to create a whole crablog application with postgresQL `docker-compose.yml` :
```yml ```yml
version: "3" version: "3"
services: services:
rubble: crablog:
image: kilerd/rubble:latest image: andreytkachenko/crablog:latest
environment: environment:
DATABASE_URL: postgres://root:password@postgres/rubble DATABASE_URL: postgres://root:password@postgres/crablog
depends_on: depends_on:
- postgres - postgres
networks: networks:
@ -70,7 +70,7 @@ services:
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password
POSTGRES_DB: rubble POSTGRES_DB: crablog
networks: networks:
- backend - backend

3
build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
println!("cargo:rerun-if-changed=./migrations");
}

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,11 +1,11 @@
version: "3" version: "3"
services: services:
rubble: crablog:
image: kilerd/rubble image: andreytkachenko/crablog
ports: ports:
- "9999:8000" - "9999:8000"
environment: environment:
DATABASE_URL: postgres://root:password@postgres/rubble DATABASE_URL: postgres://root:password@postgres/crablog
ROCKET_SECRET_KEY: Bqgzqe3zIg2siAS6IBUmL9/50GOW1xDBpxXZgSpFbyM= ROCKET_SECRET_KEY: Bqgzqe3zIg2siAS6IBUmL9/50GOW1xDBpxXZgSpFbyM=
depends_on: depends_on:
- postgres - postgres
@ -18,7 +18,7 @@ services:
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password
POSTGRES_DB: rubble POSTGRES_DB: crablog
networks: networks:
- backend - backend

View File

@ -1,3 +1,3 @@
-- Your SQL goes here -- Your SQL goes here
INSERT INTO setting ("name", "value") INSERT INTO setting ("name", "value")
VALUES ('title', 'rubble'), ('description', 'description of rubble application'); VALUES ('title', 'crablog'), ('description', 'description of crablog application');

View File

@ -1,21 +1,20 @@
use crate::pg_pool::{ManagedPgConn, Pool}; use crate::pg_pool::{DbConnection, Pool};
use r2d2::PooledConnection;
use std::sync::Arc; use std::sync::Arc;
use tera::{Context, Tera}; use tera::{Context, Tera};
#[derive(Clone)] #[derive(Clone)]
pub struct RubbleData { pub struct CrablogState {
pub pool: Pool, pub pool: Pool,
pub tera: Arc<Tera>, pub tera: Arc<Tera>,
} }
impl RubbleData { impl CrablogState {
pub fn postgres(&self) -> PooledConnection<ManagedPgConn> { pub async fn db(&self) -> DbConnection {
let pool = self.pool.clone(); DbConnection::new(self.pool.get().await.unwrap())
pool.get().unwrap()
} }
pub fn render(&self, template_name: &str, data: &Context) -> String { pub fn render(&self, template_name: &str, data: &Context) -> String {
println!("{} {:?}", template_name, data);
self.tera.render(template_name, data).unwrap() self.tera.render(template_name, data).unwrap()
} }
} }

View File

@ -1,41 +1,96 @@
use actix_web::error::ResponseError; use axum::{response::IntoResponse, Json};
use derive_more::Display; use derive_more::Display;
use actix_web::HttpResponse;
use http::StatusCode; use http::StatusCode;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug; use std::{borrow::Cow, fmt::Debug};
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum RubbleError<T> { pub enum ErrorKind {
Unauthorized(T), Unauthorized,
BadRequest(T), BadRequest,
NotFound,
InternalError,
}
pub struct Error {
kind: ErrorKind,
value: Cow<'static, str>,
}
impl Error {
pub fn new(kind: ErrorKind, msg: impl Into<Cow<'static, str>>) -> Self {
Self {
kind,
value: msg.into(),
}
}
pub fn not_found(msg: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: ErrorKind::NotFound,
value: msg.into(),
}
}
pub fn unauthorized(msg: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: ErrorKind::Unauthorized,
value: msg.into(),
}
}
pub fn bad_request(msg: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: ErrorKind::BadRequest,
value: msg.into(),
}
}
}
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let val = match self.kind {
ErrorKind::Unauthorized => (
StatusCode::UNAUTHORIZED,
Json(ErrorMsg {
message: self.value,
}),
),
ErrorKind::BadRequest => (
StatusCode::BAD_REQUEST,
Json(ErrorMsg {
message: self.value,
}),
),
ErrorKind::NotFound => (
StatusCode::NOT_FOUND,
Json(ErrorMsg {
message: self.value,
}),
),
ErrorKind::InternalError => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorMsg {
message: self.value,
}),
),
};
val.into_response()
}
}
impl<E: std::error::Error> From<E> for Error {
fn from(value: E) -> Self {
Self {
kind: ErrorKind::InternalError,
value: format!("Error: {}", value).into(),
}
}
} }
#[derive(Serialize)] #[derive(Serialize)]
struct ErrorMsg<T> { struct ErrorMsg<T> {
message: T, message: T,
} }
impl<T> ResponseError for RubbleError<T>
where
T: Debug + std::fmt::Display + Serialize,
{
fn status_code(&self) -> StatusCode {
match self {
RubbleError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
RubbleError::BadRequest(_) => StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> HttpResponse {
match self {
RubbleError::Unauthorized(message) => {
HttpResponse::Unauthorized().json(&ErrorMsg { message })
}
RubbleError::BadRequest(message) => {
HttpResponse::BadRequest().json(&ErrorMsg { message })
}
}
}
}

View File

@ -1,25 +1,17 @@
#[macro_use] #![feature(impl_trait_in_assoc_type)]
extern crate diesel;
#[macro_use]
extern crate diesel_derives;
#[macro_use]
extern crate diesel_migrations;
extern crate openssl;
use std::sync::Arc; use std::{net::SocketAddr, sync::Arc};
use actix_cors::Cors; use diesel_async::{
use actix_web::{ async_connection_wrapper::AsyncConnectionWrapper, pooled_connection::deadpool::Object,
middleware::{Logger, NormalizePath}, AsyncPgConnection,
web::{FormConfig, JsonConfig},
App, HttpServer,
}; };
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use dotenv::dotenv;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tera::Tera; use tera::Tera;
use dotenv::dotenv; use crate::{data::CrablogState, pg_pool::database_pool_establish};
use crate::{data::RubbleData, pg_pool::database_pool_establish};
mod data; mod data;
mod error; mod error;
@ -29,45 +21,57 @@ mod routers;
mod schema; mod schema;
mod utils; mod utils;
embed_migrations!(); const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
const TOKEN_KEY: Lazy<Vec<u8>> = Lazy::new(|| { static TOKEN_KEY: Lazy<Vec<u8>> = Lazy::new(|| {
std::env::var("TOKEN_KEY") std::env::var("TOKEN_KEY")
.map(|token| Vec::from(token.as_bytes())) .map(|token| Vec::from(token.as_bytes()))
.unwrap_or_else(|_| (0..32).into_iter().map(|_| rand::random::<u8>()).collect()) .unwrap_or_else(|_| (0..32).map(|_| rand::random::<u8>()).collect())
}); });
#[actix_rt::main] fn run_migrations(
async fn main() { connection: &mut impl MigrationHarness<diesel::pg::Pg>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
connection.run_pending_migrations(MIGRATIONS)?;
Ok(())
}
async fn run() -> anyhow::Result<()> {
dotenv().ok(); dotenv().ok();
pretty_env_logger::init(); pretty_env_logger::init();
let database_url = std::env::var("DATABASE_URL").expect("database_url must be set"); let database_url = std::env::var("DATABASE_URL").expect("database_url must be set");
let data = RubbleData { let data = CrablogState {
pool: database_pool_establish(&database_url), pool: database_pool_establish(&database_url).expect("cannot create pool"),
tera: Arc::new(Tera::new("templates/**/*.html").unwrap()), tera: Arc::new(Tera::new("templates/**/*.html").unwrap()),
}; };
embedded_migrations::run(&data.pool.get().expect("cannot get connection")) let async_connection = Object::take(data.pool.get().await.unwrap());
.expect("panic on embedded database migration");
println!("rubble is listening on 127.0.0.1:8000"); let mut wrapper: AsyncConnectionWrapper<AsyncPgConnection> =
AsyncConnectionWrapper::from(async_connection);
HttpServer::new(move || { let _conn = tokio::task::spawn_blocking(move || {
App::new() run_migrations(&mut wrapper).expect("panic on embedded database migration");
.app_data(data.clone()) wrapper
.data(data.clone())
.data(JsonConfig::default().limit(256_000))
.data(FormConfig::default().limit(256_000))
.wrap(Logger::default())
.wrap(Cors::default())
.wrap(NormalizePath {})
.configure(routers::routes)
}) })
.bind(("0.0.0.0", 8000)) .await?;
.unwrap()
.run() println!("crablog is listening on 0.0.0.0:8000");
.await
.unwrap() let app = routers::routes().with_state(data);
let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Ok(())
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
run().await.unwrap()
} }

View File

@ -1,11 +1,24 @@
use crate::{models::CRUD, schema::articles}; use crate::{
pg_pool::{DbConnection, DbError},
schema::articles,
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{pg::PgConnection, prelude::*, result::Error}; use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use diesel::{sql_types::Integer, Insertable, Queryable}; use diesel::{Insertable, Queryable};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Queryable, Debug, Serialize)] use futures::TryStreamExt;
pub struct All;
pub struct Published;
pub struct Pk(pub i32);
pub struct Url<'a>(pub &'a str);
use super::{Create, Delete, Query, QueryOne, Update, UpdateMany};
#[derive(Clone, Queryable, Debug, AsChangeset, Serialize, Identifiable)]
pub struct Article { pub struct Article {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
@ -18,8 +31,8 @@ pub struct Article {
pub view: i32, pub view: i32,
} }
// //
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)] #[derive(Clone, Debug, Insertable, AsChangeset, Serialize, Deserialize)]
#[table_name = "articles"] #[diesel(table_name = articles)]
pub struct NewArticle { pub struct NewArticle {
pub title: String, pub title: String,
pub body: String, pub body: String,
@ -33,52 +46,131 @@ pub struct NewArticle {
impl Article { impl Article {
pub fn link(&self) -> String { pub fn link(&self) -> String {
match self.url { match self.url {
Some(ref to) if to.len() != 0 => format!("/{}", to), Some(ref to) if !to.is_empty() => format!("/{}", to),
_ => format!("/archives/{}", self.id), _ => format!("/archives/{}", self.id),
} }
} }
}
pub fn find_by_url(conn: &PgConnection, url: &str) -> Result<Self, Error> { impl QueryOne<Pk> for DbConnection {
articles::table type Item = Article;
.filter(articles::url.eq(url))
.filter(articles::published.eq(true)) type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a;
.first::<Article>(conn)
fn query_one(&mut self, Pk(pk): Pk) -> Self::QueryOneFut<'_> {
async move { Ok(articles::table.find(pk).first::<Article>(self).await?) }
} }
}
impl<'b> QueryOne<Url<'b>> for DbConnection {
type Item = Article;
pub fn increase_view(&self, conn: &PgConnection) { type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a
diesel::sql_query(r#"UPDATE articles SET "view" = "view" + 1 where articles.id = $1"#) where 'b: 'a;
.bind::<Integer, _>(self.id)
.execute(conn) fn query_one(&mut self, Url(url): Url<'b>) -> Self::QueryOneFut<'_> {
.expect("error on incr view"); async move {
Ok(articles::table
.filter(articles::url.eq(url))
.filter(articles::published.eq(true))
.first(self)
.await?)
}
} }
} }
impl CRUD<NewArticle, NewArticle, i32> for Article { impl Query<All> for DbConnection {
fn create(conn: &PgConnection, from: &NewArticle) -> Result<Self, Error> { type Item = Article;
diesel::insert_into(articles::table)
.values(from)
.get_result(conn)
}
fn read(conn: &PgConnection) -> Vec<Self> { type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
articles::table type QueryStream<'a> = impl futures::Stream<Item = Result<Article, DbError>> + 'a;
.order(articles::publish_at.desc())
.load::<Self>(conn)
.expect("something wrong")
}
fn update(conn: &PgConnection, pk: i32, value: &NewArticle) -> Result<Self, Error> { fn query(&mut self, _query: All) -> Self::QueryFut<'_> {
diesel::update(articles::table.find(pk)) async move {
.set(value) Ok(articles::table
.get_result(conn) .load_stream::<Article>(self)
.await?
.map_err(DbError::from))
}
} }
}
fn delete(conn: &PgConnection, pk: i32) -> Result<usize, Error> { impl Query<Published> for DbConnection {
diesel::delete(articles::table.filter(articles::id.eq(pk))).execute(conn) type Item = Article;
type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
type QueryStream<'a> = impl futures::Stream<Item = Result<Article, DbError>> + 'a;
fn query(&mut self, _query: Published) -> Self::QueryFut<'_> {
async move {
Ok(articles::table
.filter(articles::columns::published.eq(true))
.load_stream(self)
.await?
.map_err(DbError::from))
}
} }
}
fn get_by_pk(conn: &PgConnection, pk: i32) -> Result<Self, Error> { impl UpdateMany<NewArticle, Pk> for DbConnection {
articles::table.find(pk).first::<Article>(conn) type Item = Article;
type UpdateManyFut<'a> =
impl futures::Future<Output = Result<Self::UpdateManyStream<'a>, DbError>> + 'a;
type UpdateManyStream<'a> = impl futures::Stream<Item = Result<Article, DbError>> + 'a;
fn update_many(&mut self, Pk(id): Pk, model: NewArticle) -> Self::UpdateManyFut<'_> {
async move {
Ok(diesel::update(articles::table)
.filter(articles::columns::id.eq(id))
.set(model)
.load_stream(self)
.await?
.map_err(DbError::from))
}
}
}
impl Create<NewArticle> for DbConnection {
type Item = Article;
type CreateFut<'a> = impl futures::Future<Output = Result<Article, DbError>> + 'a;
fn create(&mut self, model: NewArticle) -> Self::CreateFut<'_> {
async move {
Ok(diesel::insert_into(articles::table)
.values(model)
.get_result(self)
.await?)
}
}
}
impl Update<Article> for DbConnection {
type Item = Article;
type UpdateOneFut<'a> = impl futures::Future<Output = Result<Article, DbError>> + 'a;
fn update_one(&mut self, model: Article) -> Self::UpdateOneFut<'_> {
async move {
Ok(diesel::update(articles::table)
.filter(articles::columns::id.eq(model.id))
.set(model)
.get_result(self)
.await?)
}
}
}
impl Delete<Pk> for DbConnection {
type DeleteFut<'a> = impl futures::Future<Output = Result<usize, DbError>> + 'a;
fn delete(&mut self, Pk(pk): Pk) -> Self::DeleteFut<'_> {
async move {
Ok(diesel::delete(articles::table)
.filter(articles::columns::id.eq(pk))
.execute(self)
.await?)
}
} }
} }
@ -88,7 +180,7 @@ pub mod form {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NewArticleFrom { pub struct NewArticleForm {
pub title: String, pub title: String,
pub body: String, pub body: String,
pub published: bool, pub published: bool,
@ -98,8 +190,8 @@ pub mod form {
pub keywords: String, pub keywords: String,
} }
impl From<NewArticleFrom> for NewArticle { impl From<NewArticleForm> for NewArticle {
fn from(form: NewArticleFrom) -> Self { fn from(form: NewArticleForm) -> Self {
Self { Self {
title: form.title, title: form.title,
body: form.body, body: form.body,
@ -110,7 +202,7 @@ pub mod form {
keywords: if form.keywords.is_empty() { keywords: if form.keywords.is_empty() {
vec![] vec![]
} else { } else {
form.keywords.split(",").map(String::from).collect() form.keywords.split(',').map(String::from).collect()
}, },
} }
} }
@ -124,33 +216,33 @@ pub mod view {
use std::ops::Deref; use std::ops::Deref;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct ArticleView<'a> { pub struct ArticleView {
article: &'a Article, article: Article,
pub timestamp: i64, pub timestamp: i64,
pub markdown_content: String, pub markdown_content: String,
pub description: String, pub description: String,
} }
impl<'a> Deref for ArticleView<'a> { impl Deref for ArticleView {
type Target = Article; type Target = Article;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
self.article &self.article
} }
} }
impl<'a> ArticleView<'a> { impl ArticleView {
pub fn from(article: &'a Article) -> ArticleView { pub fn from(article: Article) -> ArticleView {
let content_split: Vec<_> = article.body.split("<!--more-->").collect(); let content_split: Vec<_> = article.body.split("<!--more-->").collect();
let description_parser = Parser::new(&content_split[0]); let description_parser = Parser::new(content_split[0]);
let parser = Parser::new(&article.body); let parser = Parser::new(&article.body);
let mut description_buf = String::new(); let mut description_buf = String::new();
let mut content_buf = String::new(); let mut content_buf = String::new();
html::push_html(&mut content_buf, parser); html::push_html(&mut content_buf, parser);
html::push_html(&mut description_buf, description_parser); html::push_html(&mut description_buf, description_parser);
ArticleView { ArticleView {
article,
timestamp: article.publish_at.timestamp(), timestamp: article.publish_at.timestamp(),
article,
markdown_content: content_buf, markdown_content: content_buf,
description: description_buf, description: description_buf,
} }

View File

@ -1,27 +1,80 @@
use diesel::{pg::PgConnection, result::Error}; use futures::{Future, Stream};
use crate::pg_pool::Connection;
pub mod article; pub mod article;
pub mod setting; pub mod setting;
pub mod token; pub mod token;
pub mod user; pub mod user;
pub trait CRUD<CreatedModel, UpdateModel, PK> {
fn create(conn: &PgConnection, from: &CreatedModel) -> Result<Self, Error>
where
Self: Sized;
fn read(conn: &PgConnection) -> Vec<Self> pub trait QueryOne<Q>: Connection {
type Item;
type QueryOneFut<'a>: Future<Output = Result<Self::Item, Self::Error>> + 'a
where where
Self: Sized; Self: 'a,
Q: 'a;
fn update(conn: &PgConnection, pk: PK, value: &UpdateModel) -> Result<Self, Error> fn query_one(&mut self, query: Q) -> Self::QueryOneFut<'_>;
where }
Self: Sized;
pub trait Query<Q>: Connection {
fn delete(conn: &PgConnection, pk: PK) -> Result<usize, Error> type Item;
where type QueryFut<'a>: Future<Output = Result<Self::QueryStream<'a>, Self::Error>> + 'a
Self: Sized; where
Self: 'a,
fn get_by_pk(conn: &PgConnection, pk: PK) -> Result<Self, Error> Q: 'a;
where
Self: Sized; type QueryStream<'a>: Stream<Item = Result<Self::Item, Self::Error>> + 'a
where
Self: 'a,
Q: 'a;
fn query(&mut self, query: Q) -> Self::QueryFut<'_>;
}
pub trait Update<U>: Connection {
type Item;
type UpdateOneFut<'a>: Future<Output = Result<Self::Item, Self::Error>> + 'a
where
Self: 'a,
U: 'a;
fn update_one(&mut self, model: U) -> Self::UpdateOneFut<'_>;
}
pub trait UpdateMany<U, Q>: Connection {
type Item;
type UpdateManyStream<'a>: Stream<Item = Result<Self::Item, Self::Error>> + 'a
where
Self: 'a,
U: 'a,
Q: 'a;
type UpdateManyFut<'a>: Future<Output = Result<Self::UpdateManyStream<'a>, Self::Error>> + 'a
where
Self: 'a,
U: 'a,
Q: 'a;
fn update_many(&mut self, query: Q, update: U) -> Self::UpdateManyFut<'_>;
}
pub trait Create<C>: Connection {
type Item;
type CreateFut<'a>: Future<Output = Result<Self::Item, Self::Error>> + 'a
where
Self: 'a,
C: 'a;
fn create(&mut self, model: C) -> Self::CreateFut<'_>;
}
pub trait Delete<Q>: Connection {
type DeleteFut<'a>: Future<Output = Result<usize, Self::Error>> + 'a
where
Self: 'a,
Q: 'a;
fn delete(&mut self, query: Q) -> Self::DeleteFut<'_>;
} }

View File

@ -1,10 +1,22 @@
use crate::{models::CRUD, schema::setting}; use crate::{
use diesel::{pg::PgConnection, prelude::*, result::Error, AsChangeset, Insertable, Queryable}; pg_pool::{DbConnection, DbError},
schema::setting,
};
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
use diesel_async::RunQueryDsl;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::{collections::HashMap, pin::pin};
use super::{Query, QueryOne, UpdateMany};
pub struct Map;
pub struct All;
pub struct Pk<'a>(pub &'a str);
#[derive(Queryable, Debug, Serialize, Deserialize, Insertable, AsChangeset)] #[derive(Queryable, Debug, Serialize, Deserialize, Insertable, AsChangeset)]
#[table_name = "setting"] #[diesel(table_name = setting)]
pub struct Setting { pub struct Setting {
pub name: String, pub name: String,
pub value: Option<String>, pub value: Option<String>,
@ -17,62 +29,77 @@ pub struct SettingMap {
pub url: String, pub url: String,
pub analysis: String, pub analysis: String,
} }
#[derive(Queryable, Debug, Serialize, Deserialize, AsChangeset)] #[derive(Queryable, Debug, Serialize, Deserialize, AsChangeset)]
#[table_name = "setting"] #[diesel(table_name = setting)]
pub struct UpdateSetting { pub struct UpdateSetting {
pub value: Option<String>, pub value: Option<String>,
} }
impl Setting { impl QueryOne<Map> for DbConnection {
// TODO refactor this method type Item = SettingMap;
pub fn load(conn: &PgConnection) -> SettingMap {
let settings = setting::table.load::<Setting>(conn).unwrap();
let mut settings_map: HashMap<String, String> = HashMap::new(); type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a;
for one_setting in settings { fn query_one(&mut self, _: Map) -> Self::QueryOneFut<'_> {
settings_map.insert( async move {
one_setting.name, let mut settings_map: HashMap<String, String> = HashMap::new();
one_setting.value.unwrap_or("".to_string()), let mut stream = pin!(setting::table.load_stream::<Setting>(self).await?);
);
}
SettingMap { while let Some(item) = stream.try_next().await? {
title: settings_map.get("title").unwrap_or(&"".to_string()).clone(), settings_map.insert(item.name, item.value.unwrap_or("".to_string()));
description: settings_map }
.get("description")
.unwrap_or(&"".to_string())
.clone(),
url: settings_map.get("url").unwrap_or(&"".to_string()).clone(), Ok(SettingMap {
analysis: settings_map title: settings_map.get("title").unwrap_or(&"".to_string()).clone(),
.get("analysis") description: settings_map
.unwrap_or(&"".to_string()) .get("description")
.clone(), .unwrap_or(&"".to_string())
.clone(),
url: settings_map.get("url").unwrap_or(&"".to_string()).clone(),
analysis: settings_map
.get("analysis")
.unwrap_or(&"".to_string())
.clone(),
})
} }
} }
} }
impl CRUD<(), UpdateSetting, String> for Setting { impl Query<All> for DbConnection {
fn create(_conn: &PgConnection, _from: &()) -> Result<Self, Error> { type Item = Setting;
unimplemented!()
}
fn read(_conn: &PgConnection) -> Vec<Self> { type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
unimplemented!() type QueryStream<'a> = impl futures::Stream<Item = Result<Self::Item, DbError>> + 'a;
}
fn update(conn: &PgConnection, pk: String, value: &UpdateSetting) -> Result<Self, Error> { fn query(&mut self, _query: All) -> Self::QueryFut<'_> {
diesel::update(setting::table.find(&pk)) async move {
.set(value) Ok(setting::table
.get_result(conn) .load_stream::<Setting>(self)
} .await?
.map_err(DbError::from))
fn delete(_conn: &PgConnection, _pk: String) -> Result<usize, Error> { }
unimplemented!() }
} }
fn get_by_pk(_conn: &PgConnection, _pk: String) -> Result<Self, Error> { impl<'b> UpdateMany<UpdateSetting, Pk<'b>> for DbConnection {
unimplemented!() type Item = Setting;
type UpdateManyFut<'a> = impl futures::Future<Output = Result<Self::UpdateManyStream<'a>, DbError>> + 'a
where 'b: 'a;
type UpdateManyStream<'a> = impl futures::Stream<Item = Result<Self::Item, DbError>> + 'a
where 'b: 'a;
fn update_many(&mut self, Pk(pk): Pk<'b>, model: UpdateSetting) -> Self::UpdateManyFut<'_> {
async move {
Ok(
diesel::update(setting::table.filter(setting::columns::name.eq(pk)))
.set(model)
.load_stream(self)
.await?
.map_err(DbError::from),
)
}
} }
} }

View File

@ -1,16 +1,24 @@
use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use crypto::{digest::Digest, sha3::Sha3}; use crypto::{digest::Digest, sha3::Sha3};
use diesel::{pg::PgConnection, prelude::*, result::Error, AsChangeset, Insertable, Queryable}; use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
use futures::future::{ready, Ready}; use diesel_async::RunQueryDsl;
use futures::TryStreamExt;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
data::RubbleData, error::RubbleError, models::CRUD, schema::users, utils::jwt::JWTClaims, pg_pool::{DbConnection, DbError},
schema::users,
}; };
#[derive(Queryable, Debug, Serialize, Insertable, AsChangeset, Clone)] use super::{Query, QueryOne, Update};
#[table_name = "users"]
pub struct All;
pub struct Pk(pub i32);
pub struct UserNameQuery<'a>(pub &'a str);
#[derive(Queryable, Debug, Serialize, Insertable, AsChangeset, Clone, Identifiable)]
#[diesel(table_name = users)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub username: String, pub username: String,
@ -32,80 +40,91 @@ impl User {
hasher.input_str(password); hasher.input_str(password);
hasher.result_str() hasher.result_str()
} }
}
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> { impl<'b> QueryOne<UserNameQuery<'b>> for DbConnection {
users::table type Item = User;
.filter(users::username.eq(username.to_string()))
.first::<User>(conn) type QueryOneFut<'a> = impl futures::Future<Output = Result<User, DbError>> + 'a
where 'b: 'a;
fn query_one(&mut self, query: UserNameQuery<'b>) -> Self::QueryOneFut<'_> {
async move {
Ok(users::table
.filter(users::username.eq(query.0))
.first(self)
.await?)
}
} }
} }
impl CRUD<(), User, i32> for User { impl Update<User> for DbConnection {
fn create(_conn: &PgConnection, _from: &()) -> Result<Self, Error> { type Item = User;
unreachable!()
}
fn read(_conn: &PgConnection) -> Vec<Self> { type UpdateOneFut<'a> = impl futures::Future<Output = Result<User, DbError>> + 'a;
unreachable!()
}
fn update(conn: &PgConnection, pk: i32, value: &User) -> Result<Self, Error> { fn update_one(&mut self, model: User) -> Self::UpdateOneFut<'_> {
diesel::update(users::table.find(pk)) async move {
.set(value) Ok(diesel::update(users::table.find(model.id))
.get_result(conn) .set(model)
} .get_result(self)
.await?)
fn delete(_conn: &PgConnection, _pk: i32) -> Result<usize, Error> { }
unreachable!()
}
fn get_by_pk(conn: &PgConnection, pk: i32) -> Result<Self, Error> {
users::table.filter(users::id.eq(pk)).first::<User>(conn)
} }
} }
impl FromRequest for User { impl Query<All> for DbConnection {
type Config = (); type Item = User;
type Error = RubbleError<&'static str>;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
let data = req.app_data::<RubbleData>().expect("cannot get app data"); type QueryStream<'a> = impl futures::Stream<Item = Result<User, DbError>> + 'a;
let authentication_cookie = req.cookie("Authorization");
let cookie_token = authentication_cookie.as_ref().map(|cookie| cookie.value()); fn query(&mut self, _query: All) -> Self::QueryFut<'_> {
async move { Ok(users::table.load_stream(self).await?.map_err(DbError::from)) }
let user = req
.headers()
.get("Authorization")
.map(|header| header.to_str())
.transpose()
.map(|header_token| header_token.or(cookie_token))
.map_err(|_| RubbleError::BadRequest("error on deserialize token"))
.and_then(|token| {
token.ok_or(RubbleError::Unauthorized("cannot get authentication token"))
})
.map(|jwt| jwt.splitn(2, ' ').collect::<Vec<&str>>())
.and_then(|tokens| {
if tokens.len() == 2 {
Ok(tokens[1])
} else {
Err(RubbleError::BadRequest("error on deserialize token"))
}
})
.and_then(|jwt| {
JWTClaims::decode(jwt.into())
.map_err(|_| RubbleError::Unauthorized("invalid jwt token"))
})
.and_then(|user_id| {
User::find_by_username(&data.postgres(), &user_id)
.map_err(|_| RubbleError::Unauthorized("error on get user"))
});
ready(user)
} }
} }
// impl FromRequest for User {
// type Error = Error<&'static str>;
// type Future = Ready<Result<Self, Self::Error>>;
// fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
// let data = req.app_data::<CrablogState>().expect("cannot get app data");
// let authentication_cookie = req.cookie("Authorization");
// let cookie_token = authentication_cookie.as_ref().map(|cookie| cookie.value());
// let user = req
// .headers()
// .get("Authorization")
// .map(|header| header.to_str())
// .transpose()
// .map(|header_token| header_token.or(cookie_token))
// .map_err(|_| Error::BadRequest("error on deserialize token"))
// .and_then(|token| {
// token.ok_or(Error::Unauthorized("cannot get authentication token"))
// })
// .map(|jwt| jwt.splitn(2, ' ').collect::<Vec<&str>>())
// .and_then(|tokens| {
// if tokens.len() == 2 {
// Ok(tokens[1])
// } else {
// Err(Error::BadRequest("error on deserialize token"))
// }
// })
// .and_then(|jwt| {
// JWTClaims::decode(jwt.into())
// .map_err(|_| Error::Unauthorized("invalid jwt token"))
// })
// .and_then(|user_id| {
// User::find_by_username(&mut data.postgres(), &user_id)
// .map_err(|_| Error::Unauthorized("error on get user"))
// });
// ready(user)
// }
// }
pub mod input { pub mod input {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -1,11 +1,64 @@
use diesel::{pg::PgConnection, r2d2::ConnectionManager}; use std::ops::{Deref, DerefMut};
use r2d2;
pub type ManagedPgConn = ConnectionManager<PgConnection>; use diesel_async::{
pg::AsyncPgConnection,
pooled_connection::{
deadpool::{self, BuildError, Object},
AsyncDieselConnectionManager,
},
};
pub type Pool = r2d2::Pool<ManagedPgConn>; #[derive(Debug, thiserror::Error)]
pub enum DbError {
#[error("PoolError: {0}")]
PoolError(#[from] BuildError),
pub fn database_pool_establish(database_url: &str) -> Pool { #[error("ResultError: {0}")]
let manager = ConnectionManager::<PgConnection>::new(database_url); ResultError(diesel::result::Error),
r2d2::Pool::new(manager).expect("Failed to create pool.")
#[error("Record not found")]
NotFound,
}
impl From<diesel::result::Error> for DbError {
fn from(v: diesel::result::Error) -> Self {
match v {
diesel::result::Error::NotFound => Self::NotFound,
err => Self::ResultError(err),
}
}
}
pub trait Connection: DerefMut<Target = AsyncPgConnection> {
type Error: std::error::Error;
}
pub type Pool = deadpool::Pool<AsyncPgConnection>;
pub fn database_pool_establish<S: Into<String>>(durl: S) -> Result<Pool, DbError> {
Ok(Pool::builder(AsyncDieselConnectionManager::new(durl)).build()?)
}
pub struct DbConnection(Object<AsyncPgConnection>);
impl DbConnection {
pub(crate) fn new(conn: Object<AsyncPgConnection>) -> Self {
DbConnection(conn)
}
}
impl Deref for DbConnection {
type Target = AsyncPgConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DbConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut()
}
}
impl Connection for DbConnection {
type Error = DbError;
} }

View File

@ -1,20 +1,49 @@
use actix_web::{cookie::Cookie, get, post, web, web::Form, HttpResponse, Responder}; use axum::{
async_trait,
extract::{FromRequestParts, Path, State},
response::{Html, IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use futures::stream::TryStreamExt;
use http::{request::Parts, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tera::Context; use tera::Context;
use crate::{ use crate::{
data::RubbleData, data::CrablogState,
error::Error,
models::{ models::{
article::{Article, NewArticle}, article::{self, form::NewArticleForm, Article, NewArticle},
setting::{Setting, UpdateSetting}, setting::{self, Setting, SettingMap, UpdateSetting},
user::User, user::{self, UserNameQuery},
CRUD, Create, Delete, Query, QueryOne, Update, UpdateMany,
}, },
routers::RubbleResponder, pg_pool::DbError,
utils::jwt::JWTClaims, utils::jwt::JWTClaims,
}; };
pub struct User(pub user::User);
#[async_trait]
impl FromRequestParts<CrablogState> for User {
type Rejection = (StatusCode, String);
async fn from_request_parts(
_parts: &mut Parts,
_state: &CrablogState,
) -> Result<Self, Self::Rejection> {
Ok(Self(user::User {
id: 1,
username: "demo".into(),
password: "demo".into(),
create_at: chrono::Local::now().naive_local(),
last_login_at: chrono::Local::now().naive_local(),
}))
}
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct LoginForm { pub struct LoginForm {
pub username: String, pub username: String,
@ -26,84 +55,93 @@ pub struct NewPassword {
password: String, password: String,
} }
#[get("")] // pub async fn redirect_to_admin_panel(user: Option<User>) -> impl IntoResponse {
pub async fn redirect_to_admin_panel(user: Option<User>) -> impl Responder { // if user.is_some() {
// Redirect::to("/admin/panel")
// } else {
// Redirect::to("/admin/login")
// }
// }
pub async fn admin_login(
user: Option<User>,
State(data): State<CrablogState>,
) -> Result<impl IntoResponse, Error> {
if user.is_some() { if user.is_some() {
RubbleResponder::redirect("/admin/panel") Ok(Redirect::to("/admin/panel").into_response())
} else { } else {
RubbleResponder::redirect("/admin/login") Ok(Html(data.render("admin/login.html", &Context::new())).into_response())
} }
} }
#[get("/login")]
pub async fn admin_login(user: Option<User>, data: web::Data<RubbleData>) -> impl Responder {
if user.is_some() {
RubbleResponder::redirect("/admin/panel")
} else {
RubbleResponder::html(data.render("admin/login.html", &Context::new()))
}
}
#[post("/login")]
pub async fn admin_authentication( pub async fn admin_authentication(
user: Form<LoginForm>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Form(user): Form<LoginForm>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let fetched_user = User::find_by_username(&data.postgres(), &user.username); let fetched_user = data
.db()
.await
.query_one(UserNameQuery(&user.username))
.await;
match fetched_user { Ok(match fetched_user {
Ok(login_user) => { Ok(login_user) => {
if login_user.authenticated(&user.password) { if login_user.authenticated(&user.password) {
let jwt = JWTClaims::encode(&login_user); let _jwt = JWTClaims::encode(&login_user);
HttpResponse::Found() ([
.header(http::header::LOCATION, "/admin/panel") (http::header::LOCATION, "/admin/panel"),
.cookie( // (http::header::AUTHORIZATION, format!("JWT {}", jwt))
Cookie::build("Authorization", format!("JWT {}", jwt)) ])
.path("/") .into_response()
.max_age(60 * 60 * 24 * 7) // Cookie::build("Authorization", )
.finish(), // .path("/")
) // .max_age(time::Duration::weeks(1))
.finish() // .finish(),
} else { } else {
RubbleResponder::redirect("/admin/login") Redirect::to("/admin/login").into_response()
} }
} }
Err(_) => RubbleResponder::redirect("/admin/login"), Err(_) => Redirect::to("/admin/login").into_response(),
} })
} }
#[get("/panel")] pub async fn admin_panel(
pub async fn admin_panel(user: User, data: web::Data<RubbleData>) -> impl Responder { User(user): User,
let articles = Article::read(&data.postgres()); State(data): State<CrablogState>,
let settings = Setting::load(&data.postgres()); ) -> Result<impl IntoResponse, Error> {
let mut db = data.db().await;
let articles: Vec<Article> = db.query(article::All).await?.try_collect().await?;
let settings: SettingMap = db.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("articles", &articles); context.insert("articles", &articles);
context.insert("admin", &user); context.insert("admin", &user);
RubbleResponder::html(data.render("admin/panel.html", &context)) Ok(Html(data.render("admin/panel.html", &context)))
} }
#[get("/{path}")]
pub async fn admin_show_page( pub async fn admin_show_page(
user: User, User(user): User,
path: web::Path<String>, Path(path): Path<String>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let settings = Setting::load(&data.postgres()); let settings: SettingMap = data.db().await.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("admin", &user); context.insert("admin", &user);
RubbleResponder::html(data.render(&format!("admin/{}.html", path), &context))
Ok(Html(data.render(&format!("admin/{}.html", path), &context)))
} }
#[get("/article/new")] pub async fn article_creation(
pub async fn article_creation(user: User, data: web::Data<RubbleData>) -> impl Responder { User(user): User,
let settings = Setting::load(&data.postgres()); State(data): State<CrablogState>,
) -> Result<impl IntoResponse, Error> {
let settings: SettingMap = data.db().await.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
let article = NewArticle { let article = NewArticle {
@ -111,7 +149,7 @@ pub async fn article_creation(user: User, data: web::Data<RubbleData>) -> impl R
body: String::new(), body: String::new(),
published: true, published: true,
user_id: user.id, user_id: user.id,
publish_at: Some(NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0)), publish_at: NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0),
url: None, url: None,
keywords: vec![], keywords: vec![],
}; };
@ -120,96 +158,113 @@ pub async fn article_creation(user: User, data: web::Data<RubbleData>) -> impl R
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("admin", &user); context.insert("admin", &user);
RubbleResponder::html(data.render("admin/article_add.html", &context)) Ok(Html(data.render("admin/article_add.html", &context)))
} }
#[get("/article/{article_id}")]
pub async fn article_edit( pub async fn article_edit(
user: User, User(user): User,
article_id: web::Path<i32>, Path(article_id): Path<i32>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let settings = Setting::load(&data.postgres()); let mut db = data.db().await;
let settings: SettingMap = db.query_one(setting::Map).await?;
let result = Article::get_by_pk(&data.postgres(), article_id.into_inner()); let article = match db.query_one(article::Pk(article_id)).await {
Ok(article) => article,
match result { Err(DbError::NotFound) => return Ok(Redirect::to("/admin/panel").into_response()),
Ok(article) => { Err(err) => {
let mut context = Context::new(); return Err(Error::new(
context.insert("article", &article); crate::error::ErrorKind::InternalError,
context.insert("setting", &settings); format!("Error: {}", err),
context.insert("admin", &user); ))
RubbleResponder::html(data.render("admin/article_add.html", &context))
} }
Err(_) => RubbleResponder::redirect("/admin/panel"), };
}
let mut context = Context::new();
context.insert("article", &article);
context.insert("setting", &settings);
context.insert("admin", &user);
Ok(Html(data.render("admin/article_add.html", &context)).into_response())
} }
#[post("/article")]
pub async fn article_save( pub async fn article_save(
_user: User, _user: User,
article: Form<crate::models::article::form::NewArticleFrom>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Form(article): Form<NewArticleForm>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let _article_title = article.title.clone(); let _res = data.db().await.create(article.into()).await?;
Article::create(&data.postgres(), &article.into_inner().into()); Ok(Redirect::to("/admin/panel"))
RubbleResponder::redirect("/admin/panel")
} }
#[post("/article/{aid}")]
pub async fn article_update( pub async fn article_update(
_user: User, _user: User,
aid: web::Path<i32>, Path(article_id): Path<i32>,
article: Form<crate::models::article::form::NewArticleFrom>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Form(article_form): Form<NewArticleForm>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
Article::update(&data.postgres(), *aid, &article.into_inner().into()); let _ = data
.db()
.await
.update_many(article::Pk(article_id), article_form.into())
.await;
RubbleResponder::redirect("/admin/panel") Ok(Redirect::to("/admin/panel"))
} }
#[post("/article/delete/{article_id}")]
pub async fn article_deletion( pub async fn article_deletion(
_user: User, _user: User,
article_id: web::Path<i32>, Path(article_id): Path<i32>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let i = article_id.into_inner(); let _ = data.db().await.delete(article::Pk(article_id)).await?;
Article::delete(&data.postgres(), i);
RubbleResponder::redirect("/admin/panel") Ok(Redirect::to("/admin/panel"))
} }
#[post("/password")]
pub async fn change_password( pub async fn change_password(
mut user: User, User(mut user): User,
password: web::Form<NewPassword>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Form(password): Form<NewPassword>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
user.password = User::password_generate(&password.password).to_string(); user.password = crate::models::user::User::password_generate(&password.password).to_string();
User::update(&data.postgres(), user.id, &user); data.db().await.update_one(user).await?;
RubbleResponder::redirect("/admin/panel")
Ok(Redirect::to("/admin/panel"))
} }
#[post("/setting")]
pub async fn change_setting( pub async fn change_setting(
_user: User, _user: User,
setting: web::Form<Setting>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Form(setting): Form<Setting>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let update_setting = UpdateSetting { let _ = data
value: setting.value.clone(), .db()
}; .await
Setting::update(&data.postgres(), setting.name.clone(), &update_setting); .update_many(
RubbleResponder::redirect("/admin/site-setting") setting::Pk(&setting.name),
UpdateSetting {
value: setting.value.clone(),
},
)
.await?;
Ok(Redirect::to("/admin/site-setting"))
} }
#[cfg(test)] pub fn router() -> Router<CrablogState> {
mod test { Router::new()
#[test] .route("/login", get(admin_login).post(admin_authentication))
fn test_normal() { .route("/setting", post(change_setting))
assert_eq!(1, 1); .route("/password", post(change_password))
} .route("/article/delete/:article_id", post(article_deletion))
.route(
"/article/:article_id",
get(article_edit).post(article_update),
)
.route("/article", post(article_save))
.route("/article/new", get(article_creation))
.route("/panel", get(admin_panel))
.route("/{path}", get(admin_show_page))
} }

View File

@ -1,57 +1,94 @@
use crate::{ use axum::{
data::RubbleData, extract::{Path, State},
error::RubbleError, response::IntoResponse,
models::{article::Article, user::User, CRUD}, routing::get,
routers::RubbleResponder, Json, Router,
}; };
use actix_web::{delete, get, post, put, web, Responder};
#[get("/articles")] use crate::{
pub async fn get_all_article(_user: User, data: web::Data<RubbleData>) -> impl Responder { data::CrablogState,
RubbleResponder::json(Article::read(&data.postgres())) error::Error,
models::{
article::{self, Article, NewArticle},
Create, Delete, Query, QueryOne, UpdateMany,
},
routers::admin::User,
};
use futures::TryStreamExt;
pub async fn get_all_article(
_user: User,
State(data): State<CrablogState>,
) -> Result<impl IntoResponse, Error> {
let articles: Vec<Article> = data
.db()
.await
.query(article::All)
.await?
.try_collect()
.await?;
Ok(Json(articles))
} }
#[get("/articles/{id}")]
pub async fn get_article_by_id( pub async fn get_article_by_id(
_user: User, _user: User,
id: web::Path<i32>, Path(id): Path<i32>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
Article::get_by_pk(&data.postgres(), *id) data.db()
.map(RubbleResponder::json) .await
.map_err(|_| RubbleResponder::not_found()) .query_one(article::Pk(id))
.await
.map(Json)
.map_err(|_| Error::not_found("article not found"))
} }
#[post("/articles")]
pub async fn crate_article( pub async fn crate_article(
_user: User, _user: User,
article: web::Json<crate::models::article::NewArticle>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Json(article): Json<NewArticle>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
Article::create(&data.postgres(), &article) Ok(Json(data.db().await.create(article).await?))
.map(RubbleResponder::json)
.map_err(|_| RubbleError::BadRequest("something wrong when creating article"))
} }
#[put("/articles/{id}")]
pub async fn update_article_by_id( pub async fn update_article_by_id(
_user: User, _user: User,
id: web::Path<i32>, Path(id): Path<i32>,
article: web::Json<crate::models::article::NewArticle>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Json(article): Json<NewArticle>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
Article::update(&data.postgres(), *id, &article) data.db()
.map(|data| RubbleResponder::json(data)) .await
.map_err(|_| RubbleError::BadRequest("something wrong when updating article")) .update_many(article::Pk(id), article)
.await?
.try_next()
.await
.map(Json)
.map_err(|_| Error::not_found("article not found"))
} }
#[delete("/articles/{id}")]
pub async fn delete_article_by_id( pub async fn delete_article_by_id(
_user: User, _user: User,
id: web::Path<i32>, Path(id): Path<i32>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
Article::delete(&data.postgres(), *id) data.db()
.map(|_| RubbleResponder::json("Ok")) .await
.map_err(|_| RubbleError::BadRequest("something wrong when deleting article")) .delete(article::Pk(id))
.await
.map(Json)
.map_err(|_| Error::not_found("something wrong when deleting article"))
}
pub fn router() -> Router<CrablogState> {
Router::new()
.route("/", get(get_all_article).post(crate_article))
.route(
"/:id",
get(get_article_by_id)
.put(update_article_by_id)
.delete(delete_article_by_id),
)
} }

View File

@ -1,21 +1,14 @@
use actix_web::web; use axum::Router;
use crate::data::CrablogState;
pub mod article; pub mod article;
pub mod setting; pub mod setting;
pub mod user; pub mod user;
pub fn routes(cfg: &mut web::ServiceConfig) { pub fn router() -> Router<CrablogState> {
cfg Router::new()
// user related .nest("/settings", setting::router())
.service(user::admin_authentication) .nest("/users", user::router())
.service(user::update_user_password) .nest("/articles", article::router())
// article related
.service(article::get_all_article)
.service(article::crate_article)
.service(article::get_article_by_id)
.service(article::update_article_by_id)
.service(article::delete_article_by_id)
// setting related
.service(setting::get_settings)
.service(setting::update_setting_by_key);
} }

View File

@ -1,29 +1,54 @@
use crate::{ use axum::{
data::RubbleData, extract::{Path, State},
error::RubbleError, response::IntoResponse,
models::{ routing::{get, put},
setting::{Setting, UpdateSetting}, Json, Router,
user::User,
CRUD,
},
routers::RubbleResponder,
}; };
use actix_web::{get, put, web, Responder};
#[get("/settings")] use futures::TryStreamExt;
pub async fn get_settings(_user: User, data: web::Data<RubbleData>) -> impl Responder {
RubbleResponder::json(Setting::load(&data.postgres())) use crate::{
data::CrablogState,
error::Error,
models::{
setting::{self, UpdateSetting},
Query, UpdateMany,
},
routers::admin::User,
};
pub async fn get_settings(
_user: User,
State(data): State<CrablogState>,
) -> Result<impl IntoResponse, Error> {
Ok(Json(
data.db()
.await
.query(setting::All)
.await?
.try_collect::<Vec<_>>()
.await?,
))
} }
#[put("settings/{key}")]
pub async fn update_setting_by_key( pub async fn update_setting_by_key(
_user: User, _user: User,
key: web::Path<String>, Path(key): Path<String>,
value: web::Json<UpdateSetting>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Json(value): Json<UpdateSetting>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let string = (*key).clone(); Ok(Json(
Setting::update(&data.postgres(), string, &value) data.db()
.map(RubbleResponder::json) .await
.map_err(|_| RubbleError::BadRequest("error on updating setting")) .update_many(setting::Pk(&key), value)
.await?
.try_next()
.await?,
))
}
pub fn router() -> Router<CrablogState> {
Router::new()
.route("/", get(get_settings))
.route("/:key", put(update_setting_by_key))
} }

View File

@ -1,86 +1,77 @@
//! /authentications routes
use crate::{ use crate::{
data::RubbleData, data::CrablogState,
error::RubbleError, error::Error,
models::{ models::{
token::Token, token::Token,
user::{input::LoginForm, User}, user::{self, input::LoginForm, UserNameQuery},
CRUD, QueryOne, Update,
}, },
routers::RubbleResponder, routers::admin::User,
utils::jwt::JWTClaims, utils::jwt::JWTClaims,
}; };
use actix_web::{ use axum::{
post, put, web, extract::{Path, State},
web::{Data, Json}, response::IntoResponse,
Responder, routing::{post, put},
Json, Router,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[post("/user/token")]
pub async fn admin_authentication( pub async fn admin_authentication(
user: web::Json<LoginForm>, State(data): State<CrablogState>,
data: web::Data<RubbleData>, Json(user): Json<LoginForm>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let fetched_user = User::find_by_username(&data.postgres(), &user.username); let fetched_user = data
.db()
.await
.query_one(UserNameQuery(&user.username))
.await;
match fetched_user { match fetched_user {
Ok(login_user) => { Ok(login_user) => {
if login_user.authenticated(&user.password) { if login_user.authenticated(&user.password) {
let string = JWTClaims::encode(&login_user); let string = JWTClaims::encode(&login_user);
Ok(RubbleResponder::json(Token { token: string })) Ok(Json(Token { token: string }))
} else { } else {
Err(RubbleError::Unauthorized("invalid password")) Err(Error::unauthorized("invalid password"))
} }
} }
Err(_) => Err(RubbleError::Unauthorized("invalid username")), Err(_) => Err(Error::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!()
//}
//
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct UpdatedUserPassword { pub struct UpdatedUserPassword {
pub password: String, pub password: String,
} }
#[put("/users/{id}/password")]
pub async fn update_user_password( pub async fn update_user_password(
_user: User, _user: User,
id: web::Path<String>, Path(id): Path<String>,
json: Json<UpdatedUserPassword>, State(data): State<CrablogState>,
data: Data<RubbleData>, Json(json): Json<UpdatedUserPassword>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
if json.password.eq("") { if json.password.eq("") {
return Err(RubbleError::BadRequest("password can not be empty")); return Err(Error::bad_request("password can not be empty"));
} }
let admin = User::find_by_username(&data.postgres(), &id);
admin let mut user = data
.map(|mut user| { .db()
user.password = User::password_generate(&json.password).to_string(); .await
User::update(&data.postgres(), user.id, &user); .query_one(UserNameQuery(&id))
RubbleResponder::json("OK") .await
}) .map_err(|_| Error::bad_request("cannot get admin"))?;
.map_err(|_e| RubbleError::BadRequest("cannot get admin"))
user.password = user::User::password_generate(&json.password).to_string();
data.db().await.update_one(user).await?;
Ok(Json("Ok"))
}
pub fn router() -> Router<CrablogState> {
Router::new()
.route("/token", post(admin_authentication))
.route("/:id/password", put(update_user_password))
} }

View File

@ -1,84 +1,91 @@
use crate::{ use crate::{
data::RubbleData, data::CrablogState,
error::Error,
models::{ models::{
article::{view::ArticleView, Article}, article::{self, view::ArticleView},
setting::Setting, setting, Query, QueryOne, Update,
CRUD,
}, },
routers::RubbleResponder,
}; };
use actix_web::{get, web, Responder};
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Redirect},
routing::get,
Router,
};
use futures::TryStreamExt;
use tera::Context; use tera::Context;
#[get("/")] pub async fn homepage(State(data): State<CrablogState>) -> Result<impl IntoResponse, Error> {
pub async fn homepage(data: web::Data<RubbleData>) -> impl Responder { let mut db = data.db().await;
let vec: Vec<Article> = Article::read(&data.postgres());
let article_view: Vec<_> = vec
.iter()
.filter(|article| article.published == true)
.map(ArticleView::from)
.collect();
let settings = Setting::load(&data.postgres()); let article_views: Vec<_> = db
.query(article::Published)
.await?
.map_ok(ArticleView::from)
.try_collect()
.await?;
let settings = db.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("articles", &article_view); context.insert("articles", &article_views);
RubbleResponder::html(data.render("homepage.html", &context)) Ok(Html(data.render("homepage.html", &context)))
} }
#[get("archives/{archives_id}")]
pub async fn single_article( pub async fn single_article(
archives_id: web::Path<i32>, Path(archives_id): Path<i32>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let article = Article::get_by_pk(&data.postgres(), archives_id.into_inner()); let mut db = data.db().await;
let mut article = db.query_one(article::Pk(archives_id)).await?;
if let Err(_e) = article { if let Some(ref to) = article.url {
return RubbleResponder::not_found(); if !to.is_empty() {
} return Ok(Redirect::to(&format!("/{}", to)).into_response());
let article1 = article.unwrap();
if let Some(ref to) = article1.url {
if to.len() != 0 {
return RubbleResponder::redirect(format!("/{}", to));
} }
} }
article1.increase_view(&data.postgres()); let view = ArticleView::from(article.clone());
let view = ArticleView::from(&article1);
let settings = Setting::load(&data.postgres()); article.view += 1;
db.update_one(article).await?;
let settings = db.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("article", &view); context.insert("article", &view);
RubbleResponder::html(data.render("archives.html", &context)) Ok(Html(data.render("archives.html", &context)).into_response())
} }
#[get("{url}")]
pub async fn get_article_by_url( pub async fn get_article_by_url(
url: web::Path<String>, Path(key): Path<String>,
data: web::Data<RubbleData>, State(data): State<CrablogState>,
) -> impl Responder { ) -> Result<impl IntoResponse, Error> {
let article = Article::find_by_url(&data.postgres(), &url.into_inner()); let mut db = data.db().await;
let mut article = db.query_one(article::Url(&key)).await?;
if let Err(_e) = article { let view = ArticleView::from(article.clone());
return RubbleResponder::not_found();
}
let article1 = article.unwrap();
article1.increase_view(&data.postgres());
let view = ArticleView::from(&article1); article.view += 1;
db.update_one(article).await?;
let settings = Setting::load(&data.postgres()); let settings = db.query_one(setting::Map).await?;
let mut context = Context::new(); let mut context = Context::new();
context.insert("setting", &settings); context.insert("setting", &settings);
context.insert("article", &view); context.insert("article", &view);
RubbleResponder::html(data.render("archives.html", &context)) Ok(Html(data.render("archives.html", &context)).into_response())
}
pub fn router() -> Router<CrablogState> {
Router::new()
.route("/", get(homepage))
.route("/:article", get(get_article_by_url))
.route("/archives/:archives_id", get(single_article))
} }

View File

@ -1,84 +1,21 @@
use actix_web::{middleware::NormalizePath, web, HttpResponse}; use axum::{routing::get, Router};
use serde::{Deserialize, Serialize}; use tower_http::services::{ServeDir, ServeFile};
use crate::data::CrablogState;
pub mod admin; pub mod admin;
pub mod api; pub mod api;
pub mod article; pub mod article;
pub mod rss; pub mod rss;
#[derive(Deserialize, Serialize)] pub fn routes() -> Router<CrablogState> {
pub struct JsonResponse<T> { let serve_dir = ServeDir::new("templates/resources")
data: T, .not_found_service(ServeFile::new("templates/not-found.html"));
}
Router::new()
#[derive(Deserialize, Serialize)] .nest("/", article::router())
pub struct ErrorResponse<T> { .nest("/api", api::router())
message: T, .nest("/admin", admin::router())
} .route("/rss", get(rss::rss))
.nest_service("/statics", serve_dir)
pub struct RubbleResponder;
impl RubbleResponder {
pub fn html(content: impl Into<String>) -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(content.into())
}
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 routes(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("/api").configure(api::routes))
.service(
web::scope("/admin")
.wrap(NormalizePath)
.service(admin::redirect_to_admin_panel)
.service(admin::admin_login)
.service(admin::admin_authentication)
.service(admin::admin_panel)
.service(admin::article_creation)
.service(admin::article_deletion)
.service(admin::article_edit)
.service(admin::article_save)
.service(admin::article_update)
.service(admin::change_password)
.service(admin::change_setting)
.service(admin::admin_show_page),
)
.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

@ -1,35 +1,20 @@
use crate::{ use crate::{
data::RubbleData, data::CrablogState,
error::Error,
models::{ models::{
article::{view::ArticleView, Article}, article::{self, view::ArticleView},
setting::Setting, setting, Query, QueryOne,
CRUD,
}, },
}; };
use actix_web::{get, web, HttpResponse, Responder}; use axum::{extract::State, response::IntoResponse};
use rss::{Channel, ChannelBuilder, Item, ItemBuilder}; use futures::TryStreamExt;
use http::header;
use rss::{Channel, ChannelBuilder, ItemBuilder};
use std::collections::HashMap; use std::collections::HashMap;
#[get("/rss")] pub async fn rss(State(data): State<CrablogState>) -> Result<impl IntoResponse, Error> {
pub async fn rss_(data: web::Data<RubbleData>) -> impl Responder { let mut db = data.db().await;
let articles = Article::read(&data.postgres()); let setting = db.query_one(setting::Map).await?;
let setting = Setting::load(&data.postgres());
let items: Vec<Item> = articles
.iter()
.filter(|article| article.published == true)
.map(ArticleView::from)
.map(|item| {
ItemBuilder::default()
.title(item.title.clone())
.link(format!("{}{}", setting.url, item.link()))
.description(item.description.clone())
.content(item.markdown_content.clone())
.pub_date(item.publish_at.to_string())
.build()
.unwrap()
})
.collect();
let mut namespaces: HashMap<String, String> = HashMap::new(); let mut namespaces: HashMap<String, String> = HashMap::new();
namespaces.insert( namespaces.insert(
@ -49,16 +34,38 @@ pub async fn rss_(data: web::Data<RubbleData>) -> impl Responder {
"http://search.yahoo.com/mrss/".to_string(), "http://search.yahoo.com/mrss/".to_string(),
); );
let items = db
.query(article::Published)
.await?
.map_ok(ArticleView::from)
.map_ok(|item| {
ItemBuilder::default()
.title(Some(item.title.clone()))
.link(Some(format!("{}{}", setting.url, item.link())))
.description(Some(item.description.clone()))
.content(Some(item.markdown_content.clone()))
.pub_date(Some(item.publish_at.to_string()))
.build()
.unwrap()
})
.try_collect::<Vec<_>>()
.await?;
let channel: Channel = ChannelBuilder::default() let channel: Channel = ChannelBuilder::default()
.title(setting.title) .title(setting.title)
.description(setting.description) .description(setting.description)
.generator("Rubble".to_string()) .generator(Some("Crablog".into()))
.link(setting.url.clone()) .link(setting.url.clone())
.items(items) .items(items)
.namespaces(namespaces) .namespaces(namespaces)
.build() .build()
.unwrap(); .unwrap();
HttpResponse::Ok()
.content_type("text/xml; charset=utf-8") Ok((
.body(channel.to_string()) [(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/xml; charset=utf-8"),
)],
channel.to_string(),
))
} }

View File

@ -1,3 +1,5 @@
use diesel::prelude::*;
table! { table! {
articles (id) { articles (id) {
id -> Int4, id -> Int4,

View File

@ -6,7 +6,6 @@ use jsonwebtoken::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::Add; use std::ops::Add;
use time::Duration;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct JWTClaims { pub struct JWTClaims {
@ -20,7 +19,8 @@ pub struct JWTClaims {
impl JWTClaims { impl JWTClaims {
pub fn encode(user: &User) -> String { pub fn encode(user: &User) -> String {
let now: DateTime<Utc> = Utc::now(); let now: DateTime<Utc> = Utc::now();
let expire: DateTime<Utc> = Utc::now().add(Duration::days(7)); let expire: DateTime<Utc> = Utc::now().add(chrono::Duration::weeks(1));
let claims = JWTClaims { let claims = JWTClaims {
iat: now.timestamp() as usize, iat: now.timestamp() as usize,
sub: String::from("LOGIN_TOKEN"), sub: String::from("LOGIN_TOKEN"),

View File

@ -1,102 +1,104 @@
<!doctype html> <!doctype html>
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" <meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="Content-Language" content="en"/> <meta http-equiv="Content-Language" content="en" />
<meta name="msapplication-TileColor" content="#2d89ef"> <meta name="msapplication-TileColor" content="#2d89ef">
<meta name="theme-color" content="#4188c9"> <meta name="theme-color" content="#4188c9">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="HandheldFriendly" content="True"> <meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320"> <meta name="MobileOptimized" content="320">
<!-- Generated: 2019-04-04 16:55:45 +0200 --> <!-- Generated: 2019-04-04 16:55:45 +0200 -->
<title>{% block title %} {% endblock title%}</title> <title>{% block title %} {% endblock title%}</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" <link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext"> href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext">
<script src="/statics/assets/js/require.min.js"></script> <script src="/statics/assets/js/require.min.js"></script>
<script> <script>
requirejs.config({ requirejs.config({
baseUrl: '/statics' baseUrl: '/statics'
}); });
</script> </script>
<!-- Dashboard Core --> <!-- Dashboard Core -->
<link href="/statics/assets/css/dashboard.css" rel="stylesheet"/> <link href="/statics/assets/css/dashboard.css" rel="stylesheet" />
<script src="/statics/assets/js/dashboard.js"></script> <script src="/statics/assets/js/dashboard.js"></script>
</head> </head>
<body class=""> <body class="">
<div class="page"> <div class="page">
<div class="flex-fill"> <div class="flex-fill">
<div class="header py-4"> <div class="header py-4">
<div class="container"> <div class="container">
<div class="d-flex"> <div class="d-flex">
<a class="header-brand" href="/admin/panel"> <a class="header-brand" href="/admin/panel">
{{ setting.title }} {{ setting.title }}
</a> </a>
<div class="d-flex order-lg-2 ml-auto"> <div class="d-flex order-lg-2 ml-auto">
<div class="dropdown"> <div class="dropdown">
<a href="#" class="nav-link pr-0 leading-none" data-toggle="dropdown"> <a href="#" class="nav-link pr-0 leading-none" data-toggle="dropdown">
<span class="ml-2 d-none d-lg-block"> <span class="ml-2 d-none d-lg-block">
<span class="text-default">{{ admin.username }}</span> <span class="text-default">{{ admin.username }}</span>
<small class="text-muted d-block mt-1">Administrator</small> <small class="text-muted d-block mt-1">Administrator</small>
</span> </span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow"> <div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow">
<a class="dropdown-item" href="#"> <a class="dropdown-item" href="#">
<i class="dropdown-icon fe fe-log-out"></i> Sign out <i class="dropdown-icon fe fe-log-out"></i> Sign out
</a> </a>
</div>
</div>
</div>
<a href="#" class="header-toggler d-lg-none ml-3 ml-lg-0" data-toggle="collapse"
data-target="#headerMenuCollapse">
<span class="header-toggler-icon"></span>
</a>
</div> </div>
</div>
</div> </div>
<a href="#" class="header-toggler d-lg-none ml-3 ml-lg-0" data-toggle="collapse"
data-target="#headerMenuCollapse">
<span class="header-toggler-icon"></span>
</a>
</div>
</div> </div>
<div class="header collapse d-lg-flex p-0" id="headerMenuCollapse"> </div>
<div class="container"> <div class="header collapse d-lg-flex p-0" id="headerMenuCollapse">
<div class="row align-items-center"> <div class="container">
<div class="col-lg order-lg-first"> <div class="row align-items-center">
<ul class="nav nav-tabs border-0 flex-column flex-lg-row"> <div class="col-lg order-lg-first">
<li class="nav-item"> <ul class="nav nav-tabs border-0 flex-column flex-lg-row">
<a href="/admin/panel" class="nav-link"><i class="fe fe-home"></i>Dashboard</a> <li class="nav-item">
</li> <a href="/admin/panel" class="nav-link"><i class="fe fe-home"></i>Dashboard</a>
<li class="nav-item"> </li>
<a href="/admin/administrators" class="nav-link"><i class="fe fe-home"></i>Administrators</a> <li class="nav-item">
</li> <a href="/admin/administrators" class="nav-link"><i class="fe fe-home"></i>Administrators</a>
<li class="nav-item"> </li>
<a href="/admin/site-setting" class="nav-link"><i class="fe fe-home"></i>Site Setting</a> <li class="nav-item">
</li> <a href="/admin/site-setting" class="nav-link"><i class="fe fe-home"></i>Site Setting</a>
<li class="nav-item"> </li>
<a href="{{ setting.url }}/" class="nav-link" target="_blank"><i <li class="nav-item">
class="fe fe-file-text"></i> <a href="{{ setting.url }}/" class="nav-link" target="_blank"><i class="fe fe-file-text"></i>
Go to site Go to site
</a> </a>
</li> </li>
</ul> </ul>
</div>
</div>
</div> </div>
</div>
</div> </div>
{% block content %} </div>
{% endblock content %} {% block content %}
{% endblock content %}
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="row align-items-center flex-row-reverse"> <div class="row align-items-center flex-row-reverse">
<div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center"> <div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
Powered by Rubble. Powered by Crablog.
</div> </div>
</div>
</div> </div>
</div>
</footer> </footer>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,61 +1,67 @@
<!doctype html> <!doctype html>
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta name="viewport"
<meta http-equiv="X-UA-Compatible" content="ie=edge"> content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="Content-Language" content="en" /> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="msapplication-TileColor" content="#2d89ef"> <meta http-equiv="Content-Language" content="en" />
<meta name="theme-color" content="#4188c9"> <meta name="msapplication-TileColor" content="#2d89ef">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/> <meta name="theme-color" content="#4188c9">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="HandheldFriendly" content="True"> <meta name="mobile-web-app-capable" content="yes">
<meta name="MobileOptimized" content="320"> <meta name="HandheldFriendly" content="True">
<title>Login - Rubble</title> <meta name="MobileOptimized" content="320">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <title>Login - Crablog</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="/statics/assets/js/require.min.js"></script> <link rel="stylesheet"
<script> href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&amp;subset=latin-ext">
requirejs.config({ <script src="/statics/assets/js/require.min.js"></script>
baseUrl: '/statics' <script>
}); requirejs.config({
</script> baseUrl: '/statics'
<!-- Dashboard Core --> });
<link href="/statics/assets/css/dashboard.css" rel="stylesheet" /> </script>
<script src="/statics/assets/js/dashboard.js"></script> <!-- Dashboard Core -->
<link href="/statics/assets/css/dashboard.css" rel="stylesheet" />
<script src="/statics/assets/js/dashboard.js"></script>
</head> </head>
<body class=""> <body class="">
<div class="page"> <div class="page">
<div class="page-single"> <div class="page-single">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col col-login mx-auto"> <div class="col col-login mx-auto">
<div class="text-center mb-6"> <div class="text-center mb-6">
<img src="/statics/cloud.png" class="h-8" alt=""> <img src="/statics/cloud.png" class="h-8" alt="">
</div>
<form class="card" action="" method="post">
<div class="card-body p-6">
<div class="card-title">Login to your account</div>
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" class="form-control" placeholder="Enter username" name="username">
</div>
<div class="form-group">
<label class="form-label">
Password
</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password" name="password">
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
</div>
</div>
</form>
</div>
</div> </div>
<form class="card" action="" method="post">
<div class="card-body p-6">
<div class="card-title">Login to your account</div>
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" class="form-control" placeholder="Enter username" name="username">
</div>
<div class="form-group">
<label class="form-label">
Password
</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password"
name="password">
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
</div>
</div>
</form>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,24 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="keywords" content="{% block keywords %}{% endblock keywords %}" />
<link rel="stylesheet" href="/statics/style.css">
<link rel="alternate" type="application/rss+xml" title="{{ setting.title }}" href="/rss" />
<title>{% block title %}Rubble{% endblock title %}</title>
</head>
<body>
{% block body %}
{% endblock body %}
<footer> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="keywords" content="{% block keywords %}{% endblock keywords %}" />
<link rel="stylesheet" href="/statics/style.css">
<link rel="alternate" type="application/rss+xml" title="{{ setting.title }}" href="/rss" />
<title>{% block title %}Crablog{% endblock title %}</title>
</head>
<body>
{% block body %}
{% endblock body %}
<footer>
<section class="container"> <section class="container">
<p> 自豪地使用 <a href="https://github.com/Kilerd/rubble">Project Rubble</a> 运行。 </p> <p> <a href="https://git.aidev.ru/andrey/crablog">Project Crablog</a> </p>
<div style="display:none">{{ setting.analysis | safe }}</div> <div style="display:none">{{ setting.analysis | safe }}</div>
</section> </section>
</footer> </footer>
<script src="/statics/prism.js" type="text/javascript"></script> <script src="/statics/prism.js" type="text/javascript"></script>
</body> </body>
</html> </html>

View File

@ -1,36 +1,36 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ setting.title }}{% endblock title %} {% block title %}{{ setting.title }}{% endblock title %}
{% block body %} {% block body %}
<section class="full-page"> <section class="full-page">
<header> <header>
<section class="container">
<h1>{{ setting.title }}</h1>
<p>{{ setting.description }}</p>
<nav>
<a href="/">HOMEPAGE</a>
<a href="/portfolios">PORTFOLIOS</a>
<a href="/about">ABOUT</a>
</nav>
</section>
</header>
<section class="container"> <section class="container">
{% for article in articles %} <h1>{{ setting.title }}</h1>
<section class="article"> <p>{{ setting.description }}</p>
<p class="mate">{{ article.timestamp | date(format="%B %d, %Y") }}</p> <nav>
<a class="title" <a href="/">HOMEPAGE</a>
href="{% if article.article.url %}{{ article.article.url }}{% else %}archives/{{ article.article.id }}{% endif %}">{{ <a href="/portfolios">PORTFOLIOS</a>
article.article.title }}</a> <a href="/about">ABOUT</a>
<section class="yue desc"> </nav>
{{ article.description|safe }}
</section>
</section>
{% endfor %}
</section> </section>
</header>
<section class="container">
{% for article in articles %}
<section class="article">
<p class="mate">{{ article.timestamp | date(format="%B %d, %Y") }}</p>
<a class="title"
href="{% if article.article.url %}{{ article.article.url }}{% else %}archives/{{ article.article.id }}{% endif %}">{{
article.article.title }}</a>
<section class="yue desc">
{{ article.description|safe }}
</section>
</section>
{% endfor %}
</section>
</section> </section>
{% endblock body %} {% endblock body %}

1
templates/not-found.html Normal file
View File

@ -0,0 +1 @@
qqqqq