Crablog Initial Commit
This commit is contained in:
parent
9ca9b36666
commit
0e8316588b
@ -16,6 +16,6 @@ jobs:
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- docker build -t kilerd/rubble:latest .
|
||||
- docker build -t andreytkachenko/crablog:latest .
|
||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
- docker push kilerd/rubble:latest
|
||||
- docker push andreytkachenko/crablog:latest
|
3158
Cargo.lock
generated
3158
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
55
Cargo.toml
@ -1,46 +1,47 @@
|
||||
[package]
|
||||
name = "rubble"
|
||||
name = "crablog"
|
||||
description = "a slight blog system"
|
||||
version = "0.3.3"
|
||||
authors = ["Kilerd Chan <blove694@gmail.com>"]
|
||||
version = "0.4.0"
|
||||
authors = ["Andrey Tkachenko <andrey@aidev.ru>", "Kilerd Chan <blove694@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
openssl = "0.10.27"
|
||||
|
||||
# serde
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.104"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
r2d2 = "0.8.8"
|
||||
diesel = { version = "1.4.3", features = ["postgres", "r2d2", "chrono"] }
|
||||
diesel_derives = "1.4.1"
|
||||
diesel_migrations = "1.4.0"
|
||||
diesel = { version = "2.1.3", features = ["postgres", "postgres_backend", "chrono"] }
|
||||
diesel_derives = "2.1.2"
|
||||
diesel_migrations = "2.1.0"
|
||||
|
||||
tera = "1.0.2"
|
||||
pulldown-cmark = { version = "0.6.1", default-features = false }
|
||||
tera = "1.19.1"
|
||||
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"
|
||||
|
||||
http = "0.2.0"
|
||||
actix-web= "2.0.0"
|
||||
actix-files = "0.2.1"
|
||||
futures = "0.3.1"
|
||||
http = "0.2.9"
|
||||
futures = "0.3.28"
|
||||
|
||||
# log
|
||||
log = "0.4.8"
|
||||
pretty_env_logger = "0.4.0"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5.0"
|
||||
|
||||
time = "0.1"
|
||||
rand = "0.7.3"
|
||||
time = "0.3.29"
|
||||
rand = "0.8.5"
|
||||
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"
|
||||
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"
|
||||
|
18
Dockerfile
18
Dockerfile
@ -2,8 +2,8 @@ FROM clux/muslrust:stable as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN USER=root cargo new rubble
|
||||
WORKDIR /app/rubble
|
||||
RUN USER=root cargo new crablog
|
||||
WORKDIR /app/crablog
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
@ -11,25 +11,25 @@ RUN echo 'fn main() { println!("Dummy") }' > ./src/main.rs
|
||||
|
||||
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 migrations migrations/
|
||||
COPY templates templates/
|
||||
|
||||
RUN cargo build --release --frozen --bin rubble
|
||||
RUN cargo build --release --frozen --bin crablog
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
COPY --from=builder /app/rubble/migrations /application/migrations
|
||||
COPY --from=builder /app/rubble/templates /application/templates
|
||||
COPY --from=builder /app/rubble/target/x86_64-unknown-linux-musl/release/rubble /application/rubble
|
||||
COPY --from=builder /app/crablog/migrations /application/migrations
|
||||
COPY --from=builder /app/crablog/templates /application/templates
|
||||
COPY --from=builder /app/crablog/target/x86_64-unknown-linux-musl/release/crablog /application/crablog
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENV DATABASE_URL postgres://root@postgres/rubble
|
||||
ENV DATABASE_URL postgres://root@postgres/crablog
|
||||
|
||||
WORKDIR /application
|
||||
|
||||
CMD ["./rubble"]
|
||||
CMD ["./crablog"]
|
24
README.md
24
README.md
@ -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.
|
||||
|
||||
@ -18,7 +18,7 @@ Cause this project is also the tentative staff I try to write something in Rust,
|
||||
|
||||
## 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:
|
||||
|
||||
@ -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.
|
||||
## 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`
|
||||
- Password: `password`
|
||||
|
||||
@ -39,26 +39,26 @@ after logging in, please modify the default password of admin. Then you can enjo
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
version: "3"
|
||||
services:
|
||||
rubble:
|
||||
image: kilerd/rubble:latest
|
||||
crablog:
|
||||
image: andreytkachenko/crablog:latest
|
||||
environment:
|
||||
DATABASE_URL: postgres://root:password@postgres/rubble
|
||||
DATABASE_URL: postgres://root:password@postgres/crablog
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
@ -70,7 +70,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: rubble
|
||||
POSTGRES_DB: crablog
|
||||
networks:
|
||||
- backend
|
||||
|
||||
|
3
build.rs
Normal file
3
build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=./migrations");
|
||||
}
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@ -1,11 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
rubble:
|
||||
image: kilerd/rubble
|
||||
crablog:
|
||||
image: andreytkachenko/crablog
|
||||
ports:
|
||||
- "9999:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://root:password@postgres/rubble
|
||||
DATABASE_URL: postgres://root:password@postgres/crablog
|
||||
ROCKET_SECRET_KEY: Bqgzqe3zIg2siAS6IBUmL9/50GOW1xDBpxXZgSpFbyM=
|
||||
depends_on:
|
||||
- postgres
|
||||
@ -18,7 +18,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: rubble
|
||||
POSTGRES_DB: crablog
|
||||
networks:
|
||||
- backend
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
-- Your SQL goes here
|
||||
INSERT INTO setting ("name", "value")
|
||||
VALUES ('title', 'rubble'), ('description', 'description of rubble application');
|
||||
VALUES ('title', 'crablog'), ('description', 'description of crablog application');
|
13
src/data.rs
13
src/data.rs
@ -1,21 +1,20 @@
|
||||
use crate::pg_pool::{ManagedPgConn, Pool};
|
||||
use r2d2::PooledConnection;
|
||||
use crate::pg_pool::{DbConnection, Pool};
|
||||
use std::sync::Arc;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RubbleData {
|
||||
pub struct CrablogState {
|
||||
pub pool: Pool,
|
||||
pub tera: Arc<Tera>,
|
||||
}
|
||||
|
||||
impl RubbleData {
|
||||
pub fn postgres(&self) -> PooledConnection<ManagedPgConn> {
|
||||
let pool = self.pool.clone();
|
||||
pool.get().unwrap()
|
||||
impl CrablogState {
|
||||
pub async fn db(&self) -> DbConnection {
|
||||
DbConnection::new(self.pool.get().await.unwrap())
|
||||
}
|
||||
|
||||
pub fn render(&self, template_name: &str, data: &Context) -> String {
|
||||
println!("{} {:?}", template_name, data);
|
||||
self.tera.render(template_name, data).unwrap()
|
||||
}
|
||||
}
|
||||
|
113
src/error.rs
113
src/error.rs
@ -1,41 +1,96 @@
|
||||
use actix_web::error::ResponseError;
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use derive_more::Display;
|
||||
|
||||
use actix_web::HttpResponse;
|
||||
use http::StatusCode;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum RubbleError<T> {
|
||||
Unauthorized(T),
|
||||
BadRequest(T),
|
||||
pub enum ErrorKind {
|
||||
Unauthorized,
|
||||
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)]
|
||||
struct ErrorMsg<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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
84
src/main.rs
84
src/main.rs
@ -1,25 +1,17 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_derives;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
extern crate openssl;
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{
|
||||
middleware::{Logger, NormalizePath},
|
||||
web::{FormConfig, JsonConfig},
|
||||
App, HttpServer,
|
||||
use diesel_async::{
|
||||
async_connection_wrapper::AsyncConnectionWrapper, pooled_connection::deadpool::Object,
|
||||
AsyncPgConnection,
|
||||
};
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use dotenv::dotenv;
|
||||
use once_cell::sync::Lazy;
|
||||
use tera::Tera;
|
||||
|
||||
use dotenv::dotenv;
|
||||
|
||||
use crate::{data::RubbleData, pg_pool::database_pool_establish};
|
||||
use crate::{data::CrablogState, pg_pool::database_pool_establish};
|
||||
|
||||
mod data;
|
||||
mod error;
|
||||
@ -29,45 +21,57 @@ mod routers;
|
||||
mod schema;
|
||||
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")
|
||||
.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]
|
||||
async fn main() {
|
||||
fn run_migrations(
|
||||
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();
|
||||
pretty_env_logger::init();
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("database_url must be set");
|
||||
|
||||
let data = RubbleData {
|
||||
pool: database_pool_establish(&database_url),
|
||||
let data = CrablogState {
|
||||
pool: database_pool_establish(&database_url).expect("cannot create pool"),
|
||||
tera: Arc::new(Tera::new("templates/**/*.html").unwrap()),
|
||||
};
|
||||
|
||||
embedded_migrations::run(&data.pool.get().expect("cannot get connection"))
|
||||
.expect("panic on embedded database migration");
|
||||
let async_connection = Object::take(data.pool.get().await.unwrap());
|
||||
|
||||
println!("rubble is listening on 127.0.0.1:8000");
|
||||
let mut wrapper: AsyncConnectionWrapper<AsyncPgConnection> =
|
||||
AsyncConnectionWrapper::from(async_connection);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(data.clone())
|
||||
.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)
|
||||
let _conn = tokio::task::spawn_blocking(move || {
|
||||
run_migrations(&mut wrapper).expect("panic on embedded database migration");
|
||||
wrapper
|
||||
})
|
||||
.bind(("0.0.0.0", 8000))
|
||||
.unwrap()
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
println!("crablog is listening on 0.0.0.0:8000");
|
||||
|
||||
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()
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
run().await.unwrap()
|
||||
}
|
||||
|
@ -1,11 +1,24 @@
|
||||
use crate::{models::CRUD, schema::articles};
|
||||
use crate::{
|
||||
pg_pool::{DbConnection, DbError},
|
||||
schema::articles,
|
||||
};
|
||||
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};
|
||||
|
||||
#[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 id: i32,
|
||||
pub title: String,
|
||||
@ -18,8 +31,8 @@ pub struct Article {
|
||||
pub view: i32,
|
||||
}
|
||||
//
|
||||
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)]
|
||||
#[table_name = "articles"]
|
||||
#[derive(Clone, Debug, Insertable, AsChangeset, Serialize, Deserialize)]
|
||||
#[diesel(table_name = articles)]
|
||||
pub struct NewArticle {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
@ -33,52 +46,131 @@ pub struct NewArticle {
|
||||
impl Article {
|
||||
pub fn link(&self) -> String {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_url(conn: &PgConnection, url: &str) -> Result<Self, Error> {
|
||||
articles::table
|
||||
impl QueryOne<Pk> for DbConnection {
|
||||
type Item = Article;
|
||||
|
||||
type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a;
|
||||
|
||||
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;
|
||||
|
||||
type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a
|
||||
where 'b: 'a;
|
||||
|
||||
fn query_one(&mut self, Url(url): Url<'b>) -> Self::QueryOneFut<'_> {
|
||||
async move {
|
||||
Ok(articles::table
|
||||
.filter(articles::url.eq(url))
|
||||
.filter(articles::published.eq(true))
|
||||
.first::<Article>(conn)
|
||||
.first(self)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn increase_view(&self, conn: &PgConnection) {
|
||||
diesel::sql_query(r#"UPDATE articles SET "view" = "view" + 1 where articles.id = $1"#)
|
||||
.bind::<Integer, _>(self.id)
|
||||
.execute(conn)
|
||||
.expect("error on incr view");
|
||||
}
|
||||
}
|
||||
|
||||
impl CRUD<NewArticle, NewArticle, i32> for Article {
|
||||
fn create(conn: &PgConnection, from: &NewArticle) -> Result<Self, Error> {
|
||||
diesel::insert_into(articles::table)
|
||||
.values(from)
|
||||
.get_result(conn)
|
||||
impl Query<All> for DbConnection {
|
||||
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: All) -> Self::QueryFut<'_> {
|
||||
async move {
|
||||
Ok(articles::table
|
||||
.load_stream::<Article>(self)
|
||||
.await?
|
||||
.map_err(DbError::from))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(conn: &PgConnection) -> Vec<Self> {
|
||||
articles::table
|
||||
.order(articles::publish_at.desc())
|
||||
.load::<Self>(conn)
|
||||
.expect("something wrong")
|
||||
impl Query<Published> for DbConnection {
|
||||
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 update(conn: &PgConnection, pk: i32, value: &NewArticle) -> Result<Self, Error> {
|
||||
diesel::update(articles::table.find(pk))
|
||||
.set(value)
|
||||
.get_result(conn)
|
||||
impl UpdateMany<NewArticle, Pk> for DbConnection {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(conn: &PgConnection, pk: i32) -> Result<usize, Error> {
|
||||
diesel::delete(articles::table.filter(articles::id.eq(pk))).execute(conn)
|
||||
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?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_by_pk(conn: &PgConnection, pk: i32) -> Result<Self, Error> {
|
||||
articles::table.find(pk).first::<Article>(conn)
|
||||
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};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewArticleFrom {
|
||||
pub struct NewArticleForm {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub published: bool,
|
||||
@ -98,8 +190,8 @@ pub mod form {
|
||||
pub keywords: String,
|
||||
}
|
||||
|
||||
impl From<NewArticleFrom> for NewArticle {
|
||||
fn from(form: NewArticleFrom) -> Self {
|
||||
impl From<NewArticleForm> for NewArticle {
|
||||
fn from(form: NewArticleForm) -> Self {
|
||||
Self {
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
@ -110,7 +202,7 @@ pub mod form {
|
||||
keywords: if form.keywords.is_empty() {
|
||||
vec![]
|
||||
} 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;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ArticleView<'a> {
|
||||
article: &'a Article,
|
||||
pub struct ArticleView {
|
||||
article: Article,
|
||||
pub timestamp: i64,
|
||||
pub markdown_content: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl<'a> Deref for ArticleView<'a> {
|
||||
impl Deref for ArticleView {
|
||||
type Target = Article;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.article
|
||||
&self.article
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArticleView<'a> {
|
||||
pub fn from(article: &'a Article) -> ArticleView {
|
||||
impl ArticleView {
|
||||
pub fn from(article: Article) -> ArticleView {
|
||||
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 mut description_buf = String::new();
|
||||
let mut content_buf = String::new();
|
||||
html::push_html(&mut content_buf, parser);
|
||||
html::push_html(&mut description_buf, description_parser);
|
||||
ArticleView {
|
||||
article,
|
||||
timestamp: article.publish_at.timestamp(),
|
||||
article,
|
||||
markdown_content: content_buf,
|
||||
description: description_buf,
|
||||
}
|
||||
|
@ -1,27 +1,80 @@
|
||||
use diesel::{pg::PgConnection, result::Error};
|
||||
use futures::{Future, Stream};
|
||||
|
||||
use crate::pg_pool::Connection;
|
||||
|
||||
pub mod article;
|
||||
pub mod setting;
|
||||
pub mod token;
|
||||
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
|
||||
Self: Sized;
|
||||
Self: 'a,
|
||||
Q: 'a;
|
||||
|
||||
fn update(conn: &PgConnection, pk: PK, value: &UpdateModel) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn delete(conn: &PgConnection, pk: PK) -> Result<usize, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn get_by_pk(conn: &PgConnection, pk: PK) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
fn query_one(&mut self, query: Q) -> Self::QueryOneFut<'_>;
|
||||
}
|
||||
|
||||
pub trait Query<Q>: Connection {
|
||||
type Item;
|
||||
type QueryFut<'a>: Future<Output = Result<Self::QueryStream<'a>, Self::Error>> + 'a
|
||||
where
|
||||
Self: 'a,
|
||||
Q: 'a;
|
||||
|
||||
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<'_>;
|
||||
}
|
||||
|
@ -1,10 +1,22 @@
|
||||
use crate::{models::CRUD, schema::setting};
|
||||
use diesel::{pg::PgConnection, prelude::*, result::Error, AsChangeset, Insertable, Queryable};
|
||||
use crate::{
|
||||
pg_pool::{DbConnection, DbError},
|
||||
schema::setting,
|
||||
};
|
||||
|
||||
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use futures::TryStreamExt;
|
||||
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)]
|
||||
#[table_name = "setting"]
|
||||
#[diesel(table_name = setting)]
|
||||
pub struct Setting {
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
@ -17,27 +29,28 @@ pub struct SettingMap {
|
||||
pub url: String,
|
||||
pub analysis: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug, Serialize, Deserialize, AsChangeset)]
|
||||
#[table_name = "setting"]
|
||||
#[diesel(table_name = setting)]
|
||||
pub struct UpdateSetting {
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
impl Setting {
|
||||
// TODO refactor this method
|
||||
pub fn load(conn: &PgConnection) -> SettingMap {
|
||||
let settings = setting::table.load::<Setting>(conn).unwrap();
|
||||
impl QueryOne<Map> for DbConnection {
|
||||
type Item = SettingMap;
|
||||
|
||||
type QueryOneFut<'a> = impl futures::Future<Output = Result<Self::Item, DbError>> + 'a;
|
||||
|
||||
fn query_one(&mut self, _: Map) -> Self::QueryOneFut<'_> {
|
||||
async move {
|
||||
let mut settings_map: HashMap<String, String> = HashMap::new();
|
||||
let mut stream = pin!(setting::table.load_stream::<Setting>(self).await?);
|
||||
|
||||
for one_setting in settings {
|
||||
settings_map.insert(
|
||||
one_setting.name,
|
||||
one_setting.value.unwrap_or("".to_string()),
|
||||
);
|
||||
while let Some(item) = stream.try_next().await? {
|
||||
settings_map.insert(item.name, item.value.unwrap_or("".to_string()));
|
||||
}
|
||||
|
||||
SettingMap {
|
||||
Ok(SettingMap {
|
||||
title: settings_map.get("title").unwrap_or(&"".to_string()).clone(),
|
||||
description: settings_map
|
||||
.get("description")
|
||||
@ -49,30 +62,44 @@ impl Setting {
|
||||
.get("analysis")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CRUD<(), UpdateSetting, String> for Setting {
|
||||
fn create(_conn: &PgConnection, _from: &()) -> Result<Self, Error> {
|
||||
unimplemented!()
|
||||
impl Query<All> for DbConnection {
|
||||
type Item = Setting;
|
||||
|
||||
type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
|
||||
type QueryStream<'a> = impl futures::Stream<Item = Result<Self::Item, DbError>> + 'a;
|
||||
|
||||
fn query(&mut self, _query: All) -> Self::QueryFut<'_> {
|
||||
async move {
|
||||
Ok(setting::table
|
||||
.load_stream::<Setting>(self)
|
||||
.await?
|
||||
.map_err(DbError::from))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(_conn: &PgConnection) -> Vec<Self> {
|
||||
unimplemented!()
|
||||
}
|
||||
impl<'b> UpdateMany<UpdateSetting, Pk<'b>> for DbConnection {
|
||||
type Item = Setting;
|
||||
|
||||
fn update(conn: &PgConnection, pk: String, value: &UpdateSetting) -> Result<Self, Error> {
|
||||
diesel::update(setting::table.find(&pk))
|
||||
.set(value)
|
||||
.get_result(conn)
|
||||
}
|
||||
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 delete(_conn: &PgConnection, _pk: String) -> Result<usize, Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_by_pk(_conn: &PgConnection, _pk: String) -> Result<Self, Error> {
|
||||
unimplemented!()
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,24 @@
|
||||
use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
|
||||
use chrono::NaiveDateTime;
|
||||
use crypto::{digest::Digest, sha3::Sha3};
|
||||
use diesel::{pg::PgConnection, prelude::*, result::Error, AsChangeset, Insertable, Queryable};
|
||||
use futures::future::{ready, Ready};
|
||||
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
|
||||
use diesel_async::RunQueryDsl;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use serde::Serialize;
|
||||
|
||||
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)]
|
||||
#[table_name = "users"]
|
||||
use super::{Query, QueryOne, Update};
|
||||
|
||||
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 id: i32,
|
||||
pub username: String,
|
||||
@ -32,80 +40,91 @@ impl User {
|
||||
hasher.input_str(password);
|
||||
hasher.result_str()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
|
||||
users::table
|
||||
.filter(users::username.eq(username.to_string()))
|
||||
.first::<User>(conn)
|
||||
impl<'b> QueryOne<UserNameQuery<'b>> for DbConnection {
|
||||
type Item = User;
|
||||
|
||||
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 {
|
||||
fn create(_conn: &PgConnection, _from: &()) -> Result<Self, Error> {
|
||||
unreachable!()
|
||||
}
|
||||
impl Update<User> for DbConnection {
|
||||
type Item = User;
|
||||
|
||||
fn read(_conn: &PgConnection) -> Vec<Self> {
|
||||
unreachable!()
|
||||
}
|
||||
type UpdateOneFut<'a> = impl futures::Future<Output = Result<User, DbError>> + 'a;
|
||||
|
||||
fn update(conn: &PgConnection, pk: i32, value: &User) -> Result<Self, Error> {
|
||||
diesel::update(users::table.find(pk))
|
||||
.set(value)
|
||||
.get_result(conn)
|
||||
fn update_one(&mut self, model: User) -> Self::UpdateOneFut<'_> {
|
||||
async move {
|
||||
Ok(diesel::update(users::table.find(model.id))
|
||||
.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 {
|
||||
type Config = ();
|
||||
type Error = RubbleError<&'static str>;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
impl Query<All> for DbConnection {
|
||||
type Item = User;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let data = req.app_data::<RubbleData>().expect("cannot get app data");
|
||||
let authentication_cookie = req.cookie("Authorization");
|
||||
type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
|
||||
type QueryStream<'a> = impl futures::Stream<Item = Result<User, DbError>> + 'a;
|
||||
|
||||
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(|_| 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)
|
||||
fn query(&mut self, _query: All) -> Self::QueryFut<'_> {
|
||||
async move { Ok(users::table.load_stream(self).await?.map_err(DbError::from)) }
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -1,11 +1,64 @@
|
||||
use diesel::{pg::PgConnection, r2d2::ConnectionManager};
|
||||
use r2d2;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
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 {
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||
r2d2::Pool::new(manager).expect("Failed to create pool.")
|
||||
#[error("ResultError: {0}")]
|
||||
ResultError(diesel::result::Error),
|
||||
|
||||
#[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;
|
||||
}
|
||||
|
@ -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 futures::stream::TryStreamExt;
|
||||
use http::{request::Parts, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
data::CrablogState,
|
||||
error::Error,
|
||||
models::{
|
||||
article::{Article, NewArticle},
|
||||
setting::{Setting, UpdateSetting},
|
||||
user::User,
|
||||
CRUD,
|
||||
article::{self, form::NewArticleForm, Article, NewArticle},
|
||||
setting::{self, Setting, SettingMap, UpdateSetting},
|
||||
user::{self, UserNameQuery},
|
||||
Create, Delete, Query, QueryOne, Update, UpdateMany,
|
||||
},
|
||||
routers::RubbleResponder,
|
||||
pg_pool::DbError,
|
||||
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)]
|
||||
pub struct LoginForm {
|
||||
pub username: String,
|
||||
@ -26,84 +55,93 @@ pub struct NewPassword {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
pub async fn redirect_to_admin_panel(user: Option<User>) -> impl Responder {
|
||||
// pub async fn redirect_to_admin_panel(user: Option<User>) -> impl IntoResponse {
|
||||
// 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() {
|
||||
RubbleResponder::redirect("/admin/panel")
|
||||
Ok(Redirect::to("/admin/panel").into_response())
|
||||
} 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(
|
||||
user: Form<LoginForm>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let fetched_user = User::find_by_username(&data.postgres(), &user.username);
|
||||
State(data): State<CrablogState>,
|
||||
Form(user): Form<LoginForm>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let fetched_user = data
|
||||
.db()
|
||||
.await
|
||||
.query_one(UserNameQuery(&user.username))
|
||||
.await;
|
||||
|
||||
match fetched_user {
|
||||
Ok(match fetched_user {
|
||||
Ok(login_user) => {
|
||||
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")
|
||||
.cookie(
|
||||
Cookie::build("Authorization", format!("JWT {}", jwt))
|
||||
.path("/")
|
||||
.max_age(60 * 60 * 24 * 7)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
([
|
||||
(http::header::LOCATION, "/admin/panel"),
|
||||
// (http::header::AUTHORIZATION, format!("JWT {}", jwt))
|
||||
])
|
||||
.into_response()
|
||||
// Cookie::build("Authorization", )
|
||||
// .path("/")
|
||||
// .max_age(time::Duration::weeks(1))
|
||||
// .finish(),
|
||||
} 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(user: User, data: web::Data<RubbleData>) -> impl Responder {
|
||||
let articles = Article::read(&data.postgres());
|
||||
let settings = Setting::load(&data.postgres());
|
||||
pub async fn admin_panel(
|
||||
User(user): User,
|
||||
State(data): State<CrablogState>,
|
||||
) -> 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();
|
||||
context.insert("setting", &settings);
|
||||
context.insert("articles", &articles);
|
||||
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(
|
||||
user: User,
|
||||
path: web::Path<String>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let settings = Setting::load(&data.postgres());
|
||||
User(user): User,
|
||||
Path(path): Path<String>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let settings: SettingMap = data.db().await.query_one(setting::Map).await?;
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("setting", &settings);
|
||||
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(user: User, data: web::Data<RubbleData>) -> impl Responder {
|
||||
let settings = Setting::load(&data.postgres());
|
||||
|
||||
pub async fn article_creation(
|
||||
User(user): User,
|
||||
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 article = NewArticle {
|
||||
@ -111,7 +149,7 @@ pub async fn article_creation(user: User, data: web::Data<RubbleData>) -> impl R
|
||||
body: String::new(),
|
||||
published: true,
|
||||
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,
|
||||
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("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(
|
||||
user: User,
|
||||
article_id: web::Path<i32>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let settings = Setting::load(&data.postgres());
|
||||
User(user): User,
|
||||
Path(article_id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let mut db = data.db().await;
|
||||
let settings: SettingMap = db.query_one(setting::Map).await?;
|
||||
let article = match db.query_one(article::Pk(article_id)).await {
|
||||
Ok(article) => article,
|
||||
Err(DbError::NotFound) => return Ok(Redirect::to("/admin/panel").into_response()),
|
||||
Err(err) => {
|
||||
return Err(Error::new(
|
||||
crate::error::ErrorKind::InternalError,
|
||||
format!("Error: {}", err),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let result = Article::get_by_pk(&data.postgres(), article_id.into_inner());
|
||||
|
||||
match result {
|
||||
Ok(article) => {
|
||||
let mut context = Context::new();
|
||||
|
||||
context.insert("article", &article);
|
||||
context.insert("setting", &settings);
|
||||
context.insert("admin", &user);
|
||||
RubbleResponder::html(data.render("admin/article_add.html", &context))
|
||||
}
|
||||
Err(_) => RubbleResponder::redirect("/admin/panel"),
|
||||
}
|
||||
|
||||
Ok(Html(data.render("admin/article_add.html", &context)).into_response())
|
||||
}
|
||||
|
||||
#[post("/article")]
|
||||
pub async fn article_save(
|
||||
_user: User,
|
||||
article: Form<crate::models::article::form::NewArticleFrom>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let _article_title = article.title.clone();
|
||||
State(data): State<CrablogState>,
|
||||
Form(article): Form<NewArticleForm>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let _res = data.db().await.create(article.into()).await?;
|
||||
|
||||
Article::create(&data.postgres(), &article.into_inner().into());
|
||||
|
||||
RubbleResponder::redirect("/admin/panel")
|
||||
Ok(Redirect::to("/admin/panel"))
|
||||
}
|
||||
|
||||
#[post("/article/{aid}")]
|
||||
pub async fn article_update(
|
||||
_user: User,
|
||||
aid: web::Path<i32>,
|
||||
article: Form<crate::models::article::form::NewArticleFrom>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
Article::update(&data.postgres(), *aid, &article.into_inner().into());
|
||||
Path(article_id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
Form(article_form): Form<NewArticleForm>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
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(
|
||||
_user: User,
|
||||
article_id: web::Path<i32>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let i = article_id.into_inner();
|
||||
Article::delete(&data.postgres(), i);
|
||||
Path(article_id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let _ = data.db().await.delete(article::Pk(article_id)).await?;
|
||||
|
||||
RubbleResponder::redirect("/admin/panel")
|
||||
Ok(Redirect::to("/admin/panel"))
|
||||
}
|
||||
|
||||
#[post("/password")]
|
||||
pub async fn change_password(
|
||||
mut user: User,
|
||||
password: web::Form<NewPassword>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
user.password = User::password_generate(&password.password).to_string();
|
||||
User::update(&data.postgres(), user.id, &user);
|
||||
RubbleResponder::redirect("/admin/panel")
|
||||
User(mut user): User,
|
||||
State(data): State<CrablogState>,
|
||||
Form(password): Form<NewPassword>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
user.password = crate::models::user::User::password_generate(&password.password).to_string();
|
||||
data.db().await.update_one(user).await?;
|
||||
|
||||
Ok(Redirect::to("/admin/panel"))
|
||||
}
|
||||
|
||||
#[post("/setting")]
|
||||
pub async fn change_setting(
|
||||
_user: User,
|
||||
setting: web::Form<Setting>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let update_setting = UpdateSetting {
|
||||
State(data): State<CrablogState>,
|
||||
Form(setting): Form<Setting>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let _ = data
|
||||
.db()
|
||||
.await
|
||||
.update_many(
|
||||
setting::Pk(&setting.name),
|
||||
UpdateSetting {
|
||||
value: setting.value.clone(),
|
||||
};
|
||||
Setting::update(&data.postgres(), setting.name.clone(), &update_setting);
|
||||
RubbleResponder::redirect("/admin/site-setting")
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to("/admin/site-setting"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_normal() {
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
pub fn router() -> Router<CrablogState> {
|
||||
Router::new()
|
||||
.route("/login", get(admin_login).post(admin_authentication))
|
||||
.route("/setting", post(change_setting))
|
||||
.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))
|
||||
}
|
||||
|
@ -1,57 +1,94 @@
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
error::RubbleError,
|
||||
models::{article::Article, user::User, CRUD},
|
||||
routers::RubbleResponder,
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use actix_web::{delete, get, post, put, web, Responder};
|
||||
|
||||
#[get("/articles")]
|
||||
pub async fn get_all_article(_user: User, data: web::Data<RubbleData>) -> impl Responder {
|
||||
RubbleResponder::json(Article::read(&data.postgres()))
|
||||
use crate::{
|
||||
data::CrablogState,
|
||||
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(
|
||||
_user: User,
|
||||
id: web::Path<i32>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
Article::get_by_pk(&data.postgres(), *id)
|
||||
.map(RubbleResponder::json)
|
||||
.map_err(|_| RubbleResponder::not_found())
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
data.db()
|
||||
.await
|
||||
.query_one(article::Pk(id))
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(|_| Error::not_found("article not found"))
|
||||
}
|
||||
|
||||
#[post("/articles")]
|
||||
pub async 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(|_| RubbleError::BadRequest("something wrong when creating article"))
|
||||
State(data): State<CrablogState>,
|
||||
Json(article): Json<NewArticle>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
Ok(Json(data.db().await.create(article).await?))
|
||||
}
|
||||
|
||||
#[put("/articles/{id}")]
|
||||
pub async 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(|_| RubbleError::BadRequest("something wrong when updating article"))
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
Json(article): Json<NewArticle>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
data.db()
|
||||
.await
|
||||
.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(
|
||||
_user: User,
|
||||
id: web::Path<i32>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
Article::delete(&data.postgres(), *id)
|
||||
.map(|_| RubbleResponder::json("Ok"))
|
||||
.map_err(|_| RubbleError::BadRequest("something wrong when deleting article"))
|
||||
Path(id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
data.db()
|
||||
.await
|
||||
.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),
|
||||
)
|
||||
}
|
||||
|
@ -1,21 +1,14 @@
|
||||
use actix_web::web;
|
||||
use axum::Router;
|
||||
|
||||
use crate::data::CrablogState;
|
||||
|
||||
pub mod article;
|
||||
pub mod setting;
|
||||
pub mod user;
|
||||
|
||||
pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
// user related
|
||||
.service(user::admin_authentication)
|
||||
.service(user::update_user_password)
|
||||
// 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);
|
||||
pub fn router() -> Router<CrablogState> {
|
||||
Router::new()
|
||||
.nest("/settings", setting::router())
|
||||
.nest("/users", user::router())
|
||||
.nest("/articles", article::router())
|
||||
}
|
||||
|
@ -1,29 +1,54 @@
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
error::RubbleError,
|
||||
models::{
|
||||
setting::{Setting, UpdateSetting},
|
||||
user::User,
|
||||
CRUD,
|
||||
},
|
||||
routers::RubbleResponder,
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
routing::{get, put},
|
||||
Json, Router,
|
||||
};
|
||||
use actix_web::{get, put, web, Responder};
|
||||
|
||||
#[get("/settings")]
|
||||
pub async fn get_settings(_user: User, data: web::Data<RubbleData>) -> impl Responder {
|
||||
RubbleResponder::json(Setting::load(&data.postgres()))
|
||||
use futures::TryStreamExt;
|
||||
|
||||
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(
|
||||
_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(|_| RubbleError::BadRequest("error on updating setting"))
|
||||
Path(key): Path<String>,
|
||||
State(data): State<CrablogState>,
|
||||
Json(value): Json<UpdateSetting>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
Ok(Json(
|
||||
data.db()
|
||||
.await
|
||||
.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))
|
||||
}
|
||||
|
@ -1,86 +1,77 @@
|
||||
//! /authentications routes
|
||||
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
error::RubbleError,
|
||||
data::CrablogState,
|
||||
error::Error,
|
||||
models::{
|
||||
token::Token,
|
||||
user::{input::LoginForm, User},
|
||||
CRUD,
|
||||
user::{self, input::LoginForm, UserNameQuery},
|
||||
QueryOne, Update,
|
||||
},
|
||||
routers::RubbleResponder,
|
||||
routers::admin::User,
|
||||
utils::jwt::JWTClaims,
|
||||
};
|
||||
use actix_web::{
|
||||
post, put, web,
|
||||
web::{Data, Json},
|
||||
Responder,
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
routing::{post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[post("/user/token")]
|
||||
pub async fn admin_authentication(
|
||||
user: web::Json<LoginForm>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let fetched_user = User::find_by_username(&data.postgres(), &user.username);
|
||||
State(data): State<CrablogState>,
|
||||
Json(user): Json<LoginForm>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let fetched_user = data
|
||||
.db()
|
||||
.await
|
||||
.query_one(UserNameQuery(&user.username))
|
||||
.await;
|
||||
|
||||
match fetched_user {
|
||||
Ok(login_user) => {
|
||||
if login_user.authenticated(&user.password) {
|
||||
let string = JWTClaims::encode(&login_user);
|
||||
|
||||
Ok(RubbleResponder::json(Token { token: string }))
|
||||
Ok(Json(Token { token: string }))
|
||||
} 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)]
|
||||
pub struct UpdatedUserPassword {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[put("/users/{id}/password")]
|
||||
pub async fn update_user_password(
|
||||
_user: User,
|
||||
id: web::Path<String>,
|
||||
json: Json<UpdatedUserPassword>,
|
||||
data: Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
Path(id): Path<String>,
|
||||
State(data): State<CrablogState>,
|
||||
Json(json): Json<UpdatedUserPassword>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
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
|
||||
.map(|mut user| {
|
||||
user.password = User::password_generate(&json.password).to_string();
|
||||
User::update(&data.postgres(), user.id, &user);
|
||||
RubbleResponder::json("OK")
|
||||
})
|
||||
.map_err(|_e| RubbleError::BadRequest("cannot get admin"))
|
||||
|
||||
let mut user = data
|
||||
.db()
|
||||
.await
|
||||
.query_one(UserNameQuery(&id))
|
||||
.await
|
||||
.map_err(|_| Error::bad_request("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))
|
||||
}
|
||||
|
@ -1,84 +1,91 @@
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
data::CrablogState,
|
||||
error::Error,
|
||||
models::{
|
||||
article::{view::ArticleView, Article},
|
||||
setting::Setting,
|
||||
CRUD,
|
||||
article::{self, view::ArticleView},
|
||||
setting, Query, QueryOne, Update,
|
||||
},
|
||||
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;
|
||||
|
||||
#[get("/")]
|
||||
pub async fn homepage(data: web::Data<RubbleData>) -> impl Responder {
|
||||
let vec: Vec<Article> = Article::read(&data.postgres());
|
||||
let article_view: Vec<_> = vec
|
||||
.iter()
|
||||
.filter(|article| article.published == true)
|
||||
.map(ArticleView::from)
|
||||
.collect();
|
||||
pub async fn homepage(State(data): State<CrablogState>) -> Result<impl IntoResponse, Error> {
|
||||
let mut db = data.db().await;
|
||||
|
||||
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();
|
||||
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(
|
||||
archives_id: web::Path<i32>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let article = Article::get_by_pk(&data.postgres(), archives_id.into_inner());
|
||||
Path(archives_id): Path<i32>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let mut db = data.db().await;
|
||||
let mut article = db.query_one(article::Pk(archives_id)).await?;
|
||||
|
||||
if let Err(_e) = article {
|
||||
return RubbleResponder::not_found();
|
||||
}
|
||||
let article1 = article.unwrap();
|
||||
|
||||
if let Some(ref to) = article1.url {
|
||||
if to.len() != 0 {
|
||||
return RubbleResponder::redirect(format!("/{}", to));
|
||||
if let Some(ref to) = article.url {
|
||||
if !to.is_empty() {
|
||||
return Ok(Redirect::to(&format!("/{}", to)).into_response());
|
||||
}
|
||||
}
|
||||
|
||||
article1.increase_view(&data.postgres());
|
||||
let view = ArticleView::from(&article1);
|
||||
let view = ArticleView::from(article.clone());
|
||||
|
||||
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();
|
||||
context.insert("setting", &settings);
|
||||
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(
|
||||
url: web::Path<String>,
|
||||
data: web::Data<RubbleData>,
|
||||
) -> impl Responder {
|
||||
let article = Article::find_by_url(&data.postgres(), &url.into_inner());
|
||||
Path(key): Path<String>,
|
||||
State(data): State<CrablogState>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let mut db = data.db().await;
|
||||
let mut article = db.query_one(article::Url(&key)).await?;
|
||||
|
||||
if let Err(_e) = article {
|
||||
return RubbleResponder::not_found();
|
||||
}
|
||||
let article1 = article.unwrap();
|
||||
article1.increase_view(&data.postgres());
|
||||
let view = ArticleView::from(article.clone());
|
||||
|
||||
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();
|
||||
context.insert("setting", &settings);
|
||||
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))
|
||||
}
|
||||
|
@ -1,84 +1,21 @@
|
||||
use actix_web::{middleware::NormalizePath, web, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use axum::{routing::get, Router};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
use crate::data::CrablogState;
|
||||
|
||||
pub mod admin;
|
||||
pub mod api;
|
||||
pub mod article;
|
||||
pub mod rss;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct JsonResponse<T> {
|
||||
data: T,
|
||||
}
|
||||
pub fn routes() -> Router<CrablogState> {
|
||||
let serve_dir = ServeDir::new("templates/resources")
|
||||
.not_found_service(ServeFile::new("templates/not-found.html"));
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ErrorResponse<T> {
|
||||
message: T,
|
||||
}
|
||||
|
||||
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);
|
||||
Router::new()
|
||||
.nest("/", article::router())
|
||||
.nest("/api", api::router())
|
||||
.nest("/admin", admin::router())
|
||||
.route("/rss", get(rss::rss))
|
||||
.nest_service("/statics", serve_dir)
|
||||
}
|
||||
|
@ -1,35 +1,20 @@
|
||||
use crate::{
|
||||
data::RubbleData,
|
||||
data::CrablogState,
|
||||
error::Error,
|
||||
models::{
|
||||
article::{view::ArticleView, Article},
|
||||
setting::Setting,
|
||||
CRUD,
|
||||
article::{self, view::ArticleView},
|
||||
setting, Query, QueryOne,
|
||||
},
|
||||
};
|
||||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
use rss::{Channel, ChannelBuilder, Item, ItemBuilder};
|
||||
use axum::{extract::State, response::IntoResponse};
|
||||
use futures::TryStreamExt;
|
||||
use http::header;
|
||||
use rss::{Channel, ChannelBuilder, ItemBuilder};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[get("/rss")]
|
||||
pub async fn rss_(data: web::Data<RubbleData>) -> impl Responder {
|
||||
let articles = Article::read(&data.postgres());
|
||||
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();
|
||||
pub async fn rss(State(data): State<CrablogState>) -> Result<impl IntoResponse, Error> {
|
||||
let mut db = data.db().await;
|
||||
let setting = db.query_one(setting::Map).await?;
|
||||
|
||||
let mut namespaces: HashMap<String, String> = HashMap::new();
|
||||
namespaces.insert(
|
||||
@ -49,16 +34,38 @@ pub async fn rss_(data: web::Data<RubbleData>) -> impl Responder {
|
||||
"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()
|
||||
.title(setting.title)
|
||||
.description(setting.description)
|
||||
.generator("Rubble".to_string())
|
||||
.generator(Some("Crablog".into()))
|
||||
.link(setting.url.clone())
|
||||
.items(items)
|
||||
.namespaces(namespaces)
|
||||
.build()
|
||||
.unwrap();
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/xml; charset=utf-8")
|
||||
.body(channel.to_string())
|
||||
|
||||
Ok((
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("text/xml; charset=utf-8"),
|
||||
)],
|
||||
channel.to_string(),
|
||||
))
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use diesel::prelude::*;
|
||||
|
||||
table! {
|
||||
articles (id) {
|
||||
id -> Int4,
|
||||
|
@ -6,7 +6,6 @@ use jsonwebtoken::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
use time::Duration;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JWTClaims {
|
||||
@ -20,7 +19,8 @@ pub struct JWTClaims {
|
||||
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 expire: DateTime<Utc> = Utc::now().add(chrono::Duration::weeks(1));
|
||||
|
||||
let claims = JWTClaims {
|
||||
iat: now.timestamp() as usize,
|
||||
sub: String::from("LOGIN_TOKEN"),
|
||||
|
@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
@ -28,6 +29,7 @@
|
||||
<link href="/statics/assets/css/dashboard.css" rel="stylesheet" />
|
||||
<script src="/statics/assets/js/dashboard.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<div class="page">
|
||||
|
||||
@ -75,8 +77,7 @@
|
||||
<a href="/admin/site-setting" class="nav-link"><i class="fe fe-home"></i>Site Setting</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ setting.url }}/" class="nav-link" target="_blank"><i
|
||||
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
|
||||
</a>
|
||||
</li>
|
||||
@ -92,11 +93,12 @@
|
||||
<div class="container">
|
||||
<div class="row align-items-center flex-row-reverse">
|
||||
<div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
|
||||
Powered by Rubble.
|
||||
Powered by Crablog.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,8 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<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"
|
||||
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="Content-Language" content="en" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
@ -12,9 +14,10 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<title>Login - Rubble</title>
|
||||
<title>Login - Crablog</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&subset=latin-ext">
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&subset=latin-ext">
|
||||
<script src="/statics/assets/js/require.min.js"></script>
|
||||
<script>
|
||||
requirejs.config({
|
||||
@ -25,6 +28,7 @@
|
||||
<link href="/statics/assets/css/dashboard.css" rel="stylesheet" />
|
||||
<script src="/statics/assets/js/dashboard.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<div class="page">
|
||||
<div class="page-single">
|
||||
@ -45,7 +49,8 @@
|
||||
<label class="form-label">
|
||||
Password
|
||||
</label>
|
||||
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password" name="password">
|
||||
<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>
|
||||
@ -58,4 +63,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -7,18 +8,20 @@
|
||||
<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>
|
||||
<title>{% block title %}Crablog{% endblock title %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
|
||||
<footer>
|
||||
<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>
|
||||
</section>
|
||||
</footer>
|
||||
<script src="/statics/prism.js" type="text/javascript"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1
templates/not-found.html
Normal file
1
templates/not-found.html
Normal file
@ -0,0 +1 @@
|
||||
qqqqq
|
Loading…
Reference in New Issue
Block a user