Added most relevant code for blog

This commit is contained in:
Jef Roosens 2021-11-23 09:32:08 +01:00
parent f98f0e2d4e
commit 07f25219d6
Signed by: Jef Roosens
GPG key ID: 955C0660072F691F
19 changed files with 2515 additions and 0 deletions

8
src/db/mod.rs Normal file
View file

@ -0,0 +1,8 @@
//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports
//! from poluting other modules' namespaces.
pub mod posts;
pub mod sections;
pub use posts::{NewPost, PatchPost, Post};
pub use sections::{NewSection, Section};

85
src/db/posts.rs Normal file
View file

@ -0,0 +1,85 @@
use chrono::NaiveDate;
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbOption, RbResult},
schema::{posts, posts::dsl::*},
};
#[derive(Queryable, Serialize)]
pub struct Post
{
pub id: Uuid,
pub section_id: Uuid,
pub title: Option<String>,
pub publish_date: NaiveDate,
pub content: String,
}
#[derive(Deserialize, Insertable)]
#[table_name = "posts"]
#[serde(rename_all = "camelCase")]
pub struct NewPost
{
pub section_id: Uuid,
pub title: Option<String>,
pub publish_date: NaiveDate,
pub content: String,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "posts"]
pub struct PatchPost
{
pub section_id: Option<Uuid>,
pub title: Option<String>,
pub publish_date: Option<NaiveDate>,
pub content: Option<String>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
{
Ok(posts
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
}
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
{
match posts.find(id_).first(conn) {
Ok(val) => Ok(Some(val)),
Err(diesel::NotFound) => Ok(None),
_ => Err(RbError::DbError("Couldn't find post.")),
}
}
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
{
Ok(insert_into(posts)
.values(new_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't insert post."))?)
// TODO check for conflict?
}
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
{
Ok(diesel::update(posts.filter(id.eq(post_id)))
.set(patch_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't update post."))?)
}
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
{
diesel::delete(posts.filter(id.eq(post_id)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete post."))?;
Ok(())
}

79
src/db/sections.rs Normal file
View file

@ -0,0 +1,79 @@
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::{RbError, RbResult},
schema::{sections, sections::dsl::*},
};
#[derive(Queryable, Serialize)]
pub struct Section
{
pub id: Uuid,
pub title: String,
pub shortname: String,
pub description: Option<String>,
pub is_default: bool,
pub has_titles: bool,
}
#[derive(Deserialize, Insertable)]
#[table_name = "sections"]
#[serde(rename_all = "camelCase")]
pub struct NewSection
{
title: String,
pub shortname: String,
description: Option<String>,
is_default: Option<bool>,
has_titles: Option<bool>,
}
#[derive(Deserialize, AsChangeset)]
#[table_name = "sections"]
#[serde(rename_all = "camelCase")]
pub struct PatchSection
{
title: Option<String>,
shortname: Option<String>,
description: Option<String>,
is_default: Option<bool>,
has_titles: Option<bool>,
}
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
{
Ok(sections
.offset(offset_.into())
.limit(limit_.into())
.load(conn)
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
}
pub fn create(conn: &PgConnection, new_post: &NewSection) -> RbResult<Section>
{
Ok(insert_into(sections)
.values(new_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't insert section."))?)
// TODO check for conflict?
}
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
{
Ok(diesel::update(sections.filter(id.eq(post_id)))
.set(patch_post)
.get_result(conn)
.map_err(|_| RbError::DbError("Couldn't update section."))?)
}
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
{
diesel::delete(sections.filter(id.eq(post_id)))
.execute(conn)
.map_err(|_| RbError::DbError("Couldn't delete section."))?;
Ok(())
}

0
src/lib.rs Normal file
View file

108
src/main.rs Normal file
View file

@ -0,0 +1,108 @@
// This needs to be explicitely included before diesel is imported to make sure
// compilation succeeds in the release Docker image.
extern crate openssl;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
use figment::{
providers::{Env, Format, Yaml},
Figment,
};
use rocket::{
fairing::AdHoc,
http::Status,
serde::json::{json, Value},
Build, Request, Rocket,
};
use rocket_sync_db_pools::database;
use serde::{Deserialize, Serialize};
pub mod db;
pub mod errors;
pub mod guards;
pub mod posts;
pub(crate) mod schema;
pub mod sections;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[database("postgres_rb")]
pub struct RbDbConn(diesel::PgConnection);
#[catch(default)]
fn default_catcher(status: Status, _: &Request) -> Value
{
json!({"status": status.code, "message": ""})
}
embed_migrations!();
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>>
{
let conn = RbDbConn::get_one(&rocket)
.await
.expect("database connection");
conn.run(|c| match embedded_migrations::run(c) {
Ok(()) => Ok(rocket),
Err(_) => Err(rocket),
})
.await
}
// async fn create_admin_user<'a>(rocket: &'a Rocket<Orbit>)
// {
// let config = rocket.state::<RbConfig>().expect("RbConfig instance");
// let admin_user = config.admin_user.clone();
// let admin_pass = config.admin_pass.clone();
// let conn = RbDbConn::get_one(&rocket)
// .await
// .expect("database connection");
// conn.run(move |c| {
// admin::create_admin_user(c, &admin_user, &admin_pass).expect("failed to create admin user")
// })
// .await;
// }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RbJwtConf
{
key: String,
refresh_token_size: usize,
refresh_token_expire: i64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RbConfig
{
admin_user: String,
admin_pass: String,
jwt: RbJwtConf,
}
#[launch]
fn rocket() -> _
{
let figment = Figment::from(rocket::config::Config::default())
.merge(Yaml::file("Rb.yaml").nested())
.merge(Env::prefixed("RB_").global());
// This mut is necessary when the "docs" or "web" feature is enabled, as these further modify
// the instance variable
rocket::custom(figment)
.attach(RbDbConn::fairing())
.attach(AdHoc::try_on_ignite(
"Run database migrations",
run_db_migrations,
))
// .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user))
.attach(AdHoc::config::<RbConfig>())
.register("/", catchers![default_catcher])
.mount("/sections", routes![sections::create_section])
.mount("/posts", routes![posts::get, posts::create])
}

58
src/posts.rs Normal file
View file

@ -0,0 +1,58 @@
use rocket::serde::json::Json;
use crate::{
db,
errors::{RbOption, RbResult},
guards::Admin,
RbDbConn,
};
#[get("/?<offset>&<limit>")]
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Post>>>
{
Ok(Json(
conn.run(move |c| db::posts::get(c, offset, limit)).await?,
))
}
#[post("/", data = "<new_post>")]
pub async fn create(
_admin: Admin,
conn: RbDbConn,
new_post: Json<db::NewPost>,
) -> RbResult<Json<db::Post>>
{
Ok(Json(
conn.run(move |c| db::posts::create(c, &new_post.into_inner()))
.await?,
))
}
#[get("/<id>")]
pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
{
Ok(conn
.run(move |c| db::posts::find(c, &id))
.await?
.and_then(|p| Some(Json(p))))
}
#[patch("/<id>", data = "<patch_post>")]
pub async fn patch(
_admin: Admin,
conn: RbDbConn,
id: uuid::Uuid,
patch_post: Json<db::PatchPost>,
) -> RbResult<Json<db::Post>>
{
Ok(Json(
conn.run(move |c| db::posts::update(c, &id, &patch_post.into_inner()))
.await?,
))
}
#[delete("/<id>")]
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
{
Ok(conn.run(move |c| db::posts::delete(c, &id)).await?)
}

0
src/schema.rs Normal file
View file

25
src/sections.rs Normal file
View file

@ -0,0 +1,25 @@
//! This module handles management of site sections (aka blogs).
use rocket::serde::json::Json;
use crate::{db, errors::RbResult, guards::Admin, RbDbConn};
/// Route for creating a new section.
///
/// # Arguments
///
/// * `_admin` - guard ensuring user is admin
/// * `conn` - guard providing a connection to the database
/// * `new_section` - Json-encoded NewSection object
#[post("/", data = "<new_section>")]
pub async fn create_section(
_admin: Admin,
conn: RbDbConn,
new_section: Json<db::NewSection>,
) -> RbResult<Json<db::Section>>
{
Ok(Json(
conn.run(move |c| db::sections::create(c, &new_section.into_inner()))
.await?,
))
}