Add basic db structure

This commit is contained in:
2026-01-13 13:36:57 +02:00
parent 63376af86f
commit bcbbef1a82
42 changed files with 1235 additions and 162 deletions

89
src/config.rs Normal file
View File

@@ -0,0 +1,89 @@
use anyhow::bail;
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, clap::Parser)]
pub struct CliArgs {
/// Path to config file
#[arg(long="config", short='C', default_value="./config.toml")]
config_path: camino::Utf8PathBuf,
#[arg(long="host", short='H')]
host: Option<String>,
#[arg(long="port", short='p')]
port: Option<u16>,
#[arg(long="database", short='D')]
database: Option<url::Url>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Config {
pub web: ConfigWeb,
pub database: ConfigDb
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ConfigWeb {
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ConfigDb {
host: String,
port: u16,
username: String,
password: String,
database: String,
}
impl Config {
pub fn parse() -> anyhow::Result<Self> {
let cli = CliArgs::parse();
let mut cfg = Self::default();
let mut is_new = true;
if cli.config_path.exists() {
log::info!("Config file exists, reading");
let cfg_s = std::fs::read_to_string(&cli.config_path)?;
cfg = toml::from_str(&cfg_s)?;
is_new = false;
}
cfg.web.host = cli.host.unwrap_or("0.0.0.0".to_string());
cfg.web.port = cli.port.unwrap_or(3000);
if let Some(db) = cli.database {
cfg.database.host = db.host_str().unwrap_or("0.0.0.0").to_string();
cfg.database.port = db.port().unwrap_or(5432);
cfg.database.username = db.username().to_string();
cfg.database.password = db.password().unwrap_or("").to_string();
cfg.database.database = {
match db.path_segments() {
Some(mut seg) => seg.next().unwrap_or("").to_string(),
None => String::new()
}
}
}
if is_new {
log::info!("Config file doesnt exist, creating new");
let content = toml::to_string_pretty(&cfg)?;
std::fs::write(&cli.config_path, content)?;
}
Ok(cfg)
}
pub fn database_url(&self) -> anyhow::Result<url::Url> {
let mut u = url::Url::parse("postgres://0.0.0.0/")?;
u.set_host(Some(&self.database.host))?;
let _ = u.set_port(Some(self.database.port));
let _ = u.set_username(&self.database.username);
if self.database.password.is_empty() {
let _ = u.set_password(None);
} else {
let _ = u.set_password(Some(&self.database.password));
}
u.set_path(&self.database.database);
Ok(u)
}
}

19
src/db/mod.rs Normal file
View File

@@ -0,0 +1,19 @@
use diesel::{Connection, PgConnection};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
pub mod schema;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
fn run_migrations(conn: &mut impl MigrationHarness<diesel::pg::Pg>) {
conn.run_pending_migrations(MIGRATIONS).unwrap();
log::info!("Running migrations");
}
pub fn start(cfg: &crate::config::Config) -> anyhow::Result<PgConnection> {
let mut connection = PgConnection::establish(&cfg.database_url()?.to_string())?;
run_migrations(&mut connection);
Ok(connection)
}

129
src/db/schema.rs Normal file
View File

@@ -0,0 +1,129 @@
// @generated automatically by Diesel CLI.
diesel::table! {
assigned_warehouse_managers (id) {
id -> Int8,
user_id -> Int8,
warehouse_id -> Int8,
assigned_at -> Timestamptz,
}
}
diesel::table! {
clients (id) {
id -> Int8,
email -> Text,
first_name -> Text,
last_name -> Text,
date_of_birth -> Nullable<Date>,
phone_number -> Nullable<Text>,
gov_id_number -> Nullable<Text>,
house_number -> Nullable<Text>,
address_line -> Nullable<Text>,
city -> Nullable<Text>,
state -> Nullable<Text>,
postal_code -> Nullable<Text>,
country -> Nullable<Text>,
worker_user_id -> Nullable<Int8>,
}
}
diesel::table! {
inventory (id) {
id -> Int8,
warehouse_id -> Int8,
catalog_id -> Int8,
count -> Int8,
}
}
diesel::table! {
inventory_catalog (id) {
id -> Int8,
name -> Text,
description -> Nullable<Text>,
created_at -> Timestamptz,
}
}
diesel::table! {
invoices (id) {
id -> Int8,
client_id -> Int8,
amount -> Float4,
}
}
diesel::table! {
services (id) {
id -> Int8,
name -> Text,
client_id -> Int8,
}
}
diesel::table! {
tickets (id) {
id -> Int8,
}
}
diesel::table! {
users (id) {
id -> Int8,
username -> Text,
email -> Text,
password_hash -> Text,
password_salt -> Text,
first_name -> Text,
last_name -> Text,
display_name -> Nullable<Text>,
date_of_birth -> Nullable<Date>,
phone_number -> Nullable<Text>,
created_at -> Timestamptz,
last_login_at -> Nullable<Timestamptz>,
permissions -> Numeric,
}
}
diesel::table! {
warehouse_actions (id) {
id -> Int8,
user_id -> Int8,
warehouse_id -> Int8,
count -> Int8,
reason -> Text,
timestamp -> Timestamptz,
}
}
diesel::table! {
warehouses (id) {
id -> Int8,
name -> Text,
created_at -> Timestamptz,
}
}
diesel::joinable!(assigned_warehouse_managers -> users (user_id));
diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_id));
diesel::joinable!(clients -> users (worker_user_id));
diesel::joinable!(inventory -> inventory_catalog (catalog_id));
diesel::joinable!(inventory -> warehouses (warehouse_id));
diesel::joinable!(invoices -> clients (client_id));
diesel::joinable!(services -> clients (client_id));
diesel::joinable!(warehouse_actions -> users (user_id));
diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
diesel::allow_tables_to_appear_in_same_query!(
assigned_warehouse_managers,
clients,
inventory,
inventory_catalog,
invoices,
services,
tickets,
users,
warehouse_actions,
warehouses,
);

View File

@@ -1,14 +1,24 @@
use log::LevelFilter;
mod web;
mod db;
mod config;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::builder()
.filter_module("fuck_microsoft_access", LevelFilter::Debug)
.filter_module("fma", LevelFilter::Debug)
.filter_module("diesel_migrations", LevelFilter::Debug)
.init();
let cfg = config::Config::parse()?;
unsafe {
// SAFETY: This runs while there are no other threads running, which is required for this
// function to be safe
std::env::set_var("DATABASE_URL", cfg.database_url()?.to_string());
}
web::start().await?;
let db = db::start(&cfg)?;
web::start(&cfg).await?;
Ok(())
}

132
src/schema.rs Normal file
View File

@@ -0,0 +1,132 @@
// @generated automatically by Diesel CLI.
diesel::table! {
assigned_warehouse_managers (id) {
id -> Int8,
user_id -> Int8,
warehouse_id -> Int8,
assigned_at -> Timestamptz,
}
}
diesel::table! {
clients (id) {
id -> Int8,
email -> Text,
first_name -> Text,
last_name -> Text,
date_of_birth -> Nullable<Date>,
phone_number -> Nullable<Text>,
gov_id_number -> Nullable<Text>,
house_number -> Nullable<Text>,
address_line -> Nullable<Text>,
city -> Nullable<Text>,
state -> Nullable<Text>,
postal_code -> Nullable<Text>,
country -> Nullable<Text>,
worker_user_id -> Nullable<Int8>,
}
}
diesel::table! {
inventory (id) {
id -> Int8,
warehouse_id -> Int8,
catalog_id -> Int8,
count -> Int8,
}
}
diesel::table! {
inventory_catalog (id) {
id -> Int4,
title -> Varchar,
body -> Text,
published -> Bool,
}
}
diesel::table! {
invoices (id) {
id -> Int8,
client_id -> Int8,
amount -> Float4,
}
}
diesel::table! {
services (id) {
id -> Int8,
name -> Text,
client_id -> Int8,
}
}
diesel::table! {
tickets (id) {
id -> Int4,
title -> Varchar,
body -> Text,
published -> Bool,
}
}
diesel::table! {
users (id) {
id -> Int8,
username -> Text,
email -> Text,
password_hash -> Text,
password_salt -> Text,
first_name -> Text,
last_name -> Text,
display_name -> Nullable<Text>,
date_of_birth -> Nullable<Date>,
phone_number -> Nullable<Text>,
created_at -> Timestamptz,
last_login_at -> Nullable<Timestamptz>,
permissions -> Numeric,
}
}
diesel::table! {
warehouse_actions (id) {
id -> Int8,
user_id -> Int8,
warehouse_id -> Int8,
count -> Int8,
reason -> Text,
timestamp -> Timestamptz,
}
}
diesel::table! {
warehouses (id) {
id -> Int8,
name -> Text,
created_at -> Timestamptz,
}
}
diesel::joinable!(assigned_warehouse_managers -> users (user_id));
diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_id));
diesel::joinable!(clients -> users (worker_user_id));
diesel::joinable!(inventory -> inventory_catalog (catalog_id));
diesel::joinable!(inventory -> warehouses (warehouse_id));
diesel::joinable!(invoices -> clients (client_id));
diesel::joinable!(services -> clients (client_id));
diesel::joinable!(warehouse_actions -> users (user_id));
diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
diesel::allow_tables_to_appear_in_same_query!(
assigned_warehouse_managers,
clients,
inventory,
inventory_catalog,
invoices,
services,
tickets,
users,
warehouse_actions,
warehouses,
);

View File

@@ -7,8 +7,8 @@ pub mod pages;
pub async fn start() -> anyhow::Result<()> {
let addr = "0.0.0.0:3000";
pub async fn start(cfg: &crate::config::Config) -> anyhow::Result<()> {
let addr = format!("{}:{}", cfg.web.host, cfg.web.port);
let app = Router::new()
.route("/", get(pages::home::get_page))
.route("/login", get(pages::login::get_page))
@@ -18,7 +18,7 @@ pub async fn start() -> anyhow::Result<()> {
.service(ServeDir::new("static"))
);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
log::info!("Listening on http://{addr}");
axum::serve(listener, app).await.unwrap();
Ok(())

51
src/web/pages/clients.rs Normal file
View File

@@ -0,0 +1,51 @@
use askama::Template;
use axum::response::{Html, IntoResponse, Response};
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
&mut self.ctx
}
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate {
ctx: Default::default()
};
template.set_title("Home");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;
(StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
}
}
}

View File

@@ -3,12 +3,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate {
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
pub error: String
}
impl BaseTemplate for ErrorTemplate {
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -20,7 +20,7 @@ impl BaseTemplate for ErrorTemplate {
#[axum::debug_handler]
pub async fn get_error_page(e: String) -> String {
let mut template = ErrorTemplate {
let mut template = PageTemplate {
ctx: Default::default(),
error: e.to_string()
};

View File

@@ -14,12 +14,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
pub struct HomeTemplate {
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for HomeTemplate {
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -32,7 +32,7 @@ impl BaseTemplate for HomeTemplate {
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = HomeTemplate {
let mut template = PageTemplate {
ctx: Default::default()
};

View File

@@ -0,0 +1,51 @@
use askama::Template;
use axum::response::{Html, IntoResponse, Response};
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
pub struct HomeTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for HomeTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
&mut self.ctx
}
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = HomeTemplate {
ctx: Default::default()
};
template.set_title("Home");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;
(StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
}
}
}

View File

@@ -11,12 +11,12 @@ use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "login.html")]
pub struct HomeTemplate {
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for HomeTemplate {
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
@@ -29,7 +29,7 @@ impl BaseTemplate for HomeTemplate {
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = HomeTemplate {
let mut template = PageTemplate {
ctx: Default::default()
};

View File

@@ -1,6 +1,8 @@
pub mod clients;
pub mod inventory;
pub mod tickets;
pub mod home;
pub mod login;
pub mod error;

51
src/web/pages/tickets.rs Normal file
View File

@@ -0,0 +1,51 @@
use askama::Template;
use axum::response::{Html, IntoResponse, Response};
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
&mut self.ctx
}
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate {
ctx: Default::default()
};
template.set_title("Tickets");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;
(StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
}
}
}