Initial commit

This commit is contained in:
trisua 2025-08-20 16:19:08 +00:00
commit f70b92c558
21 changed files with 6847 additions and 0 deletions

75
src/config.rs Normal file
View 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
View 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
View file

@ -0,0 +1 @@
// pub const CREATE_TABLE_ENTRIES: &str = include_str!("./create_entries.sql");

136
src/main.rs Normal file
View 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>", "&lt;/script&gt;")
.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();
}

1042
src/markdown.rs Normal file

File diff suppressed because it is too large Load diff

1
src/model.rs Normal file
View file

@ -0,0 +1 @@
//! Base types matching SQL table structures.

88
src/routes.rs Normal file
View 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
// ...