Crablog Initial Commit
This commit is contained in:
parent
9ca9b36666
commit
0e8316588b
@ -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
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]
|
[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"
|
||||||
|
18
Dockerfile
18
Dockerfile
@ -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"]
|
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.
|
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
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"
|
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
|
||||||
|
|
||||||
|
@ -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');
|
13
src/data.rs
13
src/data.rs
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
84
src/main.rs
84
src/main.rs
@ -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");
|
||||||
|
|
||||||
|
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
|
.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 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;
|
||||||
|
|
||||||
|
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::url.eq(url))
|
||||||
.filter(articles::published.eq(true))
|
.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 {
|
impl Query<All> for DbConnection {
|
||||||
fn create(conn: &PgConnection, from: &NewArticle) -> Result<Self, Error> {
|
type Item = Article;
|
||||||
diesel::insert_into(articles::table)
|
|
||||||
.values(from)
|
type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
|
||||||
.get_result(conn)
|
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> {
|
impl Query<Published> for DbConnection {
|
||||||
articles::table
|
type Item = Article;
|
||||||
.order(articles::publish_at.desc())
|
|
||||||
.load::<Self>(conn)
|
type QueryFut<'a> = impl futures::Future<Output = Result<Self::QueryStream<'a>, DbError>> + 'a;
|
||||||
.expect("something wrong")
|
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> {
|
impl UpdateMany<NewArticle, Pk> for DbConnection {
|
||||||
diesel::update(articles::table.find(pk))
|
type Item = Article;
|
||||||
.set(value)
|
|
||||||
.get_result(conn)
|
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> {
|
impl Create<NewArticle> for DbConnection {
|
||||||
diesel::delete(articles::table.filter(articles::id.eq(pk))).execute(conn)
|
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> {
|
impl Update<Article> for DbConnection {
|
||||||
articles::table.find(pk).first::<Article>(conn)
|
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,
|
||||||
}
|
}
|
||||||
|
@ -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<'_>;
|
||||||
}
|
}
|
||||||
|
@ -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,27 +29,28 @@ 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();
|
|
||||||
|
|
||||||
|
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 settings_map: HashMap<String, String> = HashMap::new();
|
||||||
|
let mut stream = pin!(setting::table.load_stream::<Setting>(self).await?);
|
||||||
|
|
||||||
for one_setting in settings {
|
while let Some(item) = stream.try_next().await? {
|
||||||
settings_map.insert(
|
settings_map.insert(item.name, item.value.unwrap_or("".to_string()));
|
||||||
one_setting.name,
|
|
||||||
one_setting.value.unwrap_or("".to_string()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingMap {
|
Ok(SettingMap {
|
||||||
title: settings_map.get("title").unwrap_or(&"".to_string()).clone(),
|
title: settings_map.get("title").unwrap_or(&"".to_string()).clone(),
|
||||||
description: settings_map
|
description: settings_map
|
||||||
.get("description")
|
.get("description")
|
||||||
@ -49,30 +62,44 @@ impl Setting {
|
|||||||
.get("analysis")
|
.get("analysis")
|
||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
.clone(),
|
.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CRUD<(), UpdateSetting, String> for Setting {
|
impl Query<All> for DbConnection {
|
||||||
fn create(_conn: &PgConnection, _from: &()) -> Result<Self, Error> {
|
type Item = Setting;
|
||||||
unimplemented!()
|
|
||||||
|
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> {
|
impl<'b> UpdateMany<UpdateSetting, Pk<'b>> for DbConnection {
|
||||||
unimplemented!()
|
type Item = Setting;
|
||||||
}
|
|
||||||
|
|
||||||
fn update(conn: &PgConnection, pk: String, value: &UpdateSetting) -> Result<Self, Error> {
|
type UpdateManyFut<'a> = impl futures::Future<Output = Result<Self::UpdateManyStream<'a>, DbError>> + 'a
|
||||||
diesel::update(setting::table.find(&pk))
|
where 'b: 'a;
|
||||||
.set(value)
|
type UpdateManyStream<'a> = impl futures::Stream<Item = Result<Self::Item, DbError>> + 'a
|
||||||
.get_result(conn)
|
where 'b: 'a;
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(_conn: &PgConnection, _pk: String) -> Result<usize, Error> {
|
fn update_many(&mut self, Pk(pk): Pk<'b>, model: UpdateSetting) -> Self::UpdateManyFut<'_> {
|
||||||
unimplemented!()
|
async move {
|
||||||
}
|
Ok(
|
||||||
|
diesel::update(setting::table.filter(setting::columns::name.eq(pk)))
|
||||||
fn get_by_pk(_conn: &PgConnection, _pk: String) -> Result<Self, Error> {
|
.set(model)
|
||||||
unimplemented!()
|
.load_stream(self)
|
||||||
|
.await?
|
||||||
|
.map_err(DbError::from),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 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();
|
let mut context = Context::new();
|
||||||
|
|
||||||
context.insert("article", &article);
|
context.insert("article", &article);
|
||||||
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)).into_response())
|
||||||
Err(_) => RubbleResponder::redirect("/admin/panel"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.update_many(
|
||||||
|
setting::Pk(&setting.name),
|
||||||
|
UpdateSetting {
|
||||||
value: setting.value.clone(),
|
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)]
|
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))
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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"));
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
Router::new()
|
||||||
pub struct ErrorResponse<T> {
|
.nest("/", article::router())
|
||||||
message: T,
|
.nest("/api", api::router())
|
||||||
}
|
.nest("/admin", admin::router())
|
||||||
|
.route("/rss", get(rss::rss))
|
||||||
pub struct RubbleResponder;
|
.nest_service("/statics", serve_dir)
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
articles (id) {
|
articles (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
@ -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"),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!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"
|
||||||
@ -28,6 +29,7 @@
|
|||||||
<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">
|
||||||
|
|
||||||
@ -75,8 +77,7 @@
|
|||||||
<a href="/admin/site-setting" class="nav-link"><i class="fe fe-home"></i>Site Setting</a>
|
<a href="/admin/site-setting" class="nav-link"><i class="fe fe-home"></i>Site Setting</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ setting.url }}/" class="nav-link" target="_blank"><i
|
<a href="{{ setting.url }}/" class="nav-link" target="_blank"><i class="fe fe-file-text"></i>
|
||||||
class="fe fe-file-text"></i>
|
|
||||||
Go to site
|
Go to site
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -92,11 +93,12 @@
|
|||||||
<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>
|
@ -1,8 +1,10 @@
|
|||||||
<!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"
|
||||||
|
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">
|
||||||
@ -12,9 +14,10 @@
|
|||||||
<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">
|
||||||
<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://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 src="/statics/assets/js/require.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
requirejs.config({
|
requirejs.config({
|
||||||
@ -25,6 +28,7 @@
|
|||||||
<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="page-single">
|
<div class="page-single">
|
||||||
@ -45,7 +49,8 @@
|
|||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Password
|
Password
|
||||||
</label>
|
</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>
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
|
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
|
||||||
@ -58,4 +63,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@ -7,18 +8,20 @@
|
|||||||
<meta name="keywords" content="{% block keywords %}{% endblock keywords %}" />
|
<meta name="keywords" content="{% block keywords %}{% endblock keywords %}" />
|
||||||
<link rel="stylesheet" href="/statics/style.css">
|
<link rel="stylesheet" href="/statics/style.css">
|
||||||
<link rel="alternate" type="application/rss+xml" title="{{ setting.title }}" href="/rss" />
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
<footer>
|
<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>
|
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