generated from t/malachite
Initial commit
This commit is contained in:
commit
fda4d01883
21 changed files with 5823 additions and 0 deletions
75
src/config.rs
Normal file
75
src/config.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use oiseau::config::{Configuration, DatabaseConfig};
|
||||
use pathbufd::PathBufD;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// The name of the site. Shown in the UI.
|
||||
#[serde(default = "default_name")]
|
||||
pub name: String,
|
||||
/// The (CSS) theme color of the site. Shown in the UI.
|
||||
#[serde(default = "default_theme_color")]
|
||||
pub theme_color: String,
|
||||
/// Real IP header (for reverse proxy).
|
||||
#[serde(default = "default_real_ip_header")]
|
||||
pub real_ip_header: String,
|
||||
/// Database configuration.
|
||||
#[serde(default = "default_database")]
|
||||
pub database: DatabaseConfig,
|
||||
}
|
||||
|
||||
fn default_name() -> String {
|
||||
"App".to_string()
|
||||
}
|
||||
|
||||
fn default_theme_color() -> String {
|
||||
"#6ee7b7".to_string()
|
||||
}
|
||||
|
||||
fn default_real_ip_header() -> String {
|
||||
"CF-Connecting-IP".to_string()
|
||||
}
|
||||
|
||||
fn default_database() -> DatabaseConfig {
|
||||
DatabaseConfig::default()
|
||||
}
|
||||
|
||||
impl Configuration for Config {
|
||||
fn db_config(&self) -> DatabaseConfig {
|
||||
self.database.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: default_name(),
|
||||
theme_color: default_theme_color(),
|
||||
real_ip_header: default_real_ip_header(),
|
||||
database: default_database(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Read the configuration file.
|
||||
pub fn read() -> Self {
|
||||
toml::from_str(
|
||||
&match std::fs::read_to_string(PathBufD::current().join("app.toml")) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
let x = Config::default();
|
||||
|
||||
std::fs::write(
|
||||
PathBufD::current().join("app.toml"),
|
||||
&toml::to_string_pretty(&x).expect("failed to serialize config"),
|
||||
)
|
||||
.expect("failed to write config");
|
||||
|
||||
return x;
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("failed to deserialize config")
|
||||
}
|
||||
}
|
29
src/database/mod.rs
Normal file
29
src/database/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
mod sql;
|
||||
|
||||
use crate::config::Config;
|
||||
use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult};
|
||||
use tetratto_core::model::{Error, Result};
|
||||
|
||||
pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataManager(pub OiseauManager<Config>);
|
||||
|
||||
impl DataManager {
|
||||
/// Create a new [`DataManager`].
|
||||
pub async fn new(config: Config) -> PgResult<Self> {
|
||||
Ok(Self(OiseauManager::new(config).await?))
|
||||
}
|
||||
|
||||
/// Initialize tables.
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
// execute!(&conn, sql::CREATE_TABLE_ENTRIES).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
1
src/database/sql/mod.rs
Normal file
1
src/database/sql/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
// pub const CREATE_TABLE_ENTRIES: &str = include_str!("./create_entries.sql");
|
136
src/main.rs
Normal file
136
src/main.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
mod config;
|
||||
mod database;
|
||||
mod markdown;
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
use crate::database::DataManager;
|
||||
use axum::{Extension, Router};
|
||||
use config::Config;
|
||||
use nanoneo::core::element::Render;
|
||||
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
|
||||
use tera::{Tera, Value};
|
||||
use tetratto_core::html;
|
||||
use tetratto_shared::hash::salt;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_http::{
|
||||
catch_panic::CatchPanicLayer,
|
||||
trace::{self, TraceLayer},
|
||||
};
|
||||
use tracing::{Level, info};
|
||||
|
||||
pub(crate) type InnerState = (DataManager, Tera, String);
|
||||
pub(crate) type State = Arc<RwLock<InnerState>>;
|
||||
|
||||
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
Ok(markdown::render_markdown(value.as_str().unwrap())
|
||||
.replace("\\@", "@")
|
||||
.replace("%5C@", "@")
|
||||
.into())
|
||||
}
|
||||
|
||||
fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
Ok(value
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.replace("</script>", "</script>")
|
||||
.into())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! create_dir_if_not_exists {
|
||||
($dir_path:expr) => {
|
||||
if !std::fs::exists(&$dir_path).unwrap() {
|
||||
std::fs::create_dir($dir_path).unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
dotenv::dotenv().ok();
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.init();
|
||||
|
||||
let port = match var("PORT") {
|
||||
Ok(port) => port.parse::<u16>().expect("port should be a u16"),
|
||||
Err(_) => 8020,
|
||||
};
|
||||
|
||||
// ...
|
||||
let database = DataManager::new(Config::read())
|
||||
.await
|
||||
.expect("failed to connect to database");
|
||||
database.init().await.expect("failed to init database");
|
||||
|
||||
// build lisp
|
||||
create_dir_if_not_exists!("./templates_build");
|
||||
create_dir_if_not_exists!("./icons");
|
||||
|
||||
for x in glob::glob("./templates_src/**/*").expect("failed to read pattern") {
|
||||
match x {
|
||||
Ok(x) => std::fs::write(
|
||||
x.to_str()
|
||||
.unwrap()
|
||||
.replace("templates_src/", "templates_build/"),
|
||||
html::pull_icons(
|
||||
nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
|
||||
.render(&mut HashMap::new()),
|
||||
"./icons",
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.expect("failed to write template"),
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// create docs dir
|
||||
create_dir_if_not_exists!("./docs");
|
||||
|
||||
// ...
|
||||
let mut tera = match Tera::new(&format!("./templates_build/**/*")) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
println!("{e}");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
tera.register_filter("markdown", render_markdown);
|
||||
tera.register_filter("remove_script_tags", remove_script_tags);
|
||||
|
||||
// create app
|
||||
let app = Router::new()
|
||||
.merge(routes::routes())
|
||||
.layer(Extension(Arc::new(RwLock::new((database, tera, salt())))))
|
||||
.layer(axum::extract::DefaultBodyLimit::max(
|
||||
var("BODY_LIMIT")
|
||||
.unwrap_or("8388608".to_string())
|
||||
.parse::<usize>()
|
||||
.unwrap(),
|
||||
))
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||
)
|
||||
.layer(CatchPanicLayer::new());
|
||||
|
||||
// ...
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("🪨 malachite.");
|
||||
info!("listening on http://0.0.0.0:{}", port);
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
18
src/markdown.rs
Normal file
18
src/markdown.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
pub fn render_markdown(input: &str) -> String {
|
||||
let html = tetratto_shared::markdown::render_markdown_dirty(input);
|
||||
|
||||
let mut allowed_attributes = HashSet::new();
|
||||
allowed_attributes.insert("id");
|
||||
allowed_attributes.insert("class");
|
||||
allowed_attributes.insert("ref");
|
||||
allowed_attributes.insert("aria-label");
|
||||
allowed_attributes.insert("lang");
|
||||
allowed_attributes.insert("title");
|
||||
allowed_attributes.insert("align");
|
||||
allowed_attributes.insert("src");
|
||||
allowed_attributes.insert("style");
|
||||
|
||||
tetratto_shared::markdown::clean_html(html, allowed_attributes)
|
||||
}
|
1
src/model.rs
Normal file
1
src/model.rs
Normal file
|
@ -0,0 +1 @@
|
|||
//! Base types matching SQL table structures.
|
88
src/routes.rs
Normal file
88
src/routes.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use crate::{State, config::Config};
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
extract::Path,
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, get_service},
|
||||
};
|
||||
use pathbufd::PathBufD;
|
||||
use tera::Context;
|
||||
use tetratto_core::model::Error;
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
.nest_service(
|
||||
"/public",
|
||||
get_service(tower_http::services::ServeDir::new("./public")),
|
||||
)
|
||||
.fallback(not_found_request)
|
||||
.route("/docs/{name}", get(view_doc_request))
|
||||
// pages
|
||||
.route("/", get(index_request))
|
||||
// api
|
||||
// ...
|
||||
}
|
||||
|
||||
fn default_context(config: &Config, build_code: &str) -> Context {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("name", &config.name);
|
||||
ctx.insert("theme_color", &config.theme_color);
|
||||
ctx.insert("build_code", &build_code);
|
||||
ctx
|
||||
}
|
||||
|
||||
// pages
|
||||
async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
|
||||
let mut ctx = default_context(&data.0.0, &build_code);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("page".to_string()).to_string(),
|
||||
);
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
Html(
|
||||
tera.render("index.lisp", &default_context(&data.0.0, &build_code))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn view_doc_request(
|
||||
Extension(data): Extension<State>,
|
||||
Path(name): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||
let path = PathBufD::current().extend(&["docs", &format!("{name}.md")]);
|
||||
|
||||
if !std::fs::exists(&path).unwrap_or(false) {
|
||||
let mut ctx = default_context(&data.0.0, &build_code);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
||||
);
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
let mut ctx = default_context(&data.0.0, &build_code);
|
||||
ctx.insert("error", &Error::MiscError(e.to_string()).to_string());
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctx = default_context(&data.0.0, &build_code);
|
||||
|
||||
ctx.insert("text", &text);
|
||||
ctx.insert("file_name", &name);
|
||||
|
||||
return Html(tera.render("doc.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
// api
|
||||
// ...
|
Loading…
Add table
Add a link
Reference in a new issue