Layed groundwork for microservice split
This commit is contained in:
parent
e2003442e2
commit
0b048eb8b0
44 changed files with 461 additions and 259 deletions
2
blog/.env
Normal file
2
blog/.env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# This file is used by diesel to find the development database
|
||||
DATABASE_URL=postgres://rb:rb@localhost:5433/rb
|
||||
16
blog/Cargo.toml
Normal file
16
blog/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "blog"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "rb_blog"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rb-blog"
|
||||
path = "src/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
5
blog/diesel.toml
Normal file
5
blog/diesel.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
0
blog/migrations/.gitkeep
Normal file
0
blog/migrations/.gitkeep
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||
36
blog/migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
blog/migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS users, refresh_tokens CASCADE;
|
||||
23
blog/migrations/2021-08-20-110251_users-and-auth/up.sql
Normal file
23
blog/migrations/2021-08-20-110251_users-and-auth/up.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE users (
|
||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
|
||||
username varchar(32) UNIQUE NOT NULL,
|
||||
-- Hashed + salted representation of the username
|
||||
password text NOT NULL,
|
||||
-- Wether the user is currently blocked
|
||||
blocked boolean NOT NULL DEFAULT false,
|
||||
-- Wether the user is an admin
|
||||
admin boolean NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
-- Stores refresh tokens
|
||||
CREATE TABLE refresh_tokens (
|
||||
-- This is more efficient than storing the text
|
||||
token bytea PRIMARY KEY,
|
||||
-- The user for whom the token was created
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
-- When the token expires
|
||||
expires_at timestamp NOT NULL,
|
||||
-- When the token was last used (is NULL until used)
|
||||
last_used_at timestamp
|
||||
);
|
||||
7
blog/migrations/2021-09-13-143540_sections/down.sql
Normal file
7
blog/migrations/2021-09-13-143540_sections/down.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
drop trigger insert_enforce_post_titles on posts;
|
||||
drop trigger update_enforce_post_titles on posts;
|
||||
drop function enforce_post_titles;
|
||||
|
||||
drop table posts cascade;
|
||||
drop table sections cascade;
|
||||
58
blog/migrations/2021-09-13-143540_sections/up.sql
Normal file
58
blog/migrations/2021-09-13-143540_sections/up.sql
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
-- Your SQL goes here
|
||||
create table sections (
|
||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
|
||||
-- Title of the section
|
||||
title varchar(255) UNIQUE NOT NULL,
|
||||
-- Name to use when routing (this just makes for prettier URLs)
|
||||
shortname varchar(32) UNIQUE NOT NULL,
|
||||
-- Optional description of the section
|
||||
description text,
|
||||
-- Wether to show the section in the default list on the homepage
|
||||
is_default boolean NOT NULL DEFAULT false,
|
||||
-- Wether the posts should contain titles or not
|
||||
has_titles boolean NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
create table posts (
|
||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
|
||||
section_id uuid NOT NULL REFERENCES sections(id) ON DELETE CASCADE,
|
||||
-- Title of the post
|
||||
-- Wether this is NULL or not is enforced using the enforce_post_titles trigger
|
||||
title varchar(255),
|
||||
-- Post date, defaults to today
|
||||
publish_date date NOT NULL DEFAULT now(),
|
||||
-- Content of the post
|
||||
content text NOT NULL
|
||||
);
|
||||
|
||||
create function enforce_post_titles() returns trigger as $enforce_post_titles$
|
||||
begin
|
||||
-- Check for a wrongfully null title
|
||||
if new.title is null and exists (
|
||||
select 1 from sections where id = new.section_id and has_titles
|
||||
) then
|
||||
raise exception 'Expected a post title, but got null.';
|
||||
end if;
|
||||
|
||||
if new.title is not null and exists (
|
||||
select 1 from sections where id = new.section_id and not has_titles
|
||||
) then
|
||||
raise exception 'Expected an empty post title, but got a value.';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$enforce_post_titles$ language plpgsql;
|
||||
|
||||
create trigger insert_enforce_post_titles
|
||||
before insert on posts
|
||||
for each row
|
||||
execute function enforce_post_titles();
|
||||
|
||||
create trigger update_enforce_post_titles
|
||||
before update of title on posts
|
||||
for each row
|
||||
when (old.title is distinct from new.title)
|
||||
execute function enforce_post_titles();
|
||||
0
blog/src/lib.rs
Normal file
0
blog/src/lib.rs
Normal file
146
blog/src/main.rs
Normal file
146
blog/src/main.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// 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,
|
||||
};
|
||||
#[cfg(any(feature = "web", feature = "docs"))]
|
||||
use rocket::fs;
|
||||
use rocket::{
|
||||
fairing::AdHoc,
|
||||
http::Status,
|
||||
serde::json::{json, Value},
|
||||
Build, Orbit, Request, Rocket,
|
||||
};
|
||||
use rocket_sync_db_pools::database;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod admin;
|
||||
pub mod auth;
|
||||
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
|
||||
#[allow(unused_mut)]
|
||||
let mut instance = 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(
|
||||
"/api/auth",
|
||||
routes![auth::already_logged_in, auth::login, auth::refresh_token,],
|
||||
)
|
||||
.mount(
|
||||
"/api/admin",
|
||||
routes![admin::create_user, admin::get_user_info],
|
||||
)
|
||||
.mount("/api/sections", routes![sections::create_section])
|
||||
.mount("/api/posts", routes![posts::get, posts::create]);
|
||||
|
||||
// It's weird that this is allowed, but the line on its own isn't
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
instance = instance.mount(
|
||||
"/",
|
||||
fs::FileServer::new(
|
||||
"/var/www/html/web",
|
||||
fs::Options::Index | fs::Options::NormalizeDirs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "docs")]
|
||||
{
|
||||
instance = instance.mount(
|
||||
"/docs",
|
||||
fs::FileServer::new(
|
||||
"/var/www/html/docs",
|
||||
fs::Options::Index | fs::Options::NormalizeDirs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
instance
|
||||
}
|
||||
58
blog/src/posts.rs
Normal file
58
blog/src/posts.rs
Normal 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
blog/src/schema.rs
Normal file
0
blog/src/schema.rs
Normal file
25
blog/src/sections.rs
Normal file
25
blog/src/sections.rs
Normal 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?,
|
||||
))
|
||||
}
|
||||
Reference in a new issue