Compare commits

..

7 commits

Author SHA1 Message Date
Simon Bruder b8184c459b
Improve page title layout with long titles
All checks were successful
/ build (push) Successful in 6s
2024-07-24 12:20:23 +02:00
Simon Bruder 026f13e7e0
Use clap for configuration 2024-07-24 12:20:22 +02:00
Simon Bruder 2eb3b505e0
Add item state 2024-07-24 12:20:19 +02:00
Simon Bruder 35230b6c37
flake: Update pre-commit-hooks.nix to new name
All checks were successful
/ build (push) Successful in 33s
This also removes all input overrides.
2024-07-24 12:19:59 +02:00
Simon Bruder faeec629a0
CI: Only gate OCI image push on branch 2024-07-24 12:19:58 +02:00
Simon Bruder 27ebe5770a
Remove home link from navbar
There is nothing there and it can also be reached by clicking on the
branding.
2024-07-24 12:19:58 +02:00
Simon Bruder e83bc8316e
Move away from models and manage subpackage
This architecture was started when the project still used Diesel.
Now that it uses SQLx, less things are done in Rust and more are done in
SQL. This commit now moves more of the query logic into SQL, which
should lead to more efficient queries and less moving data around.
2024-07-24 12:19:55 +02:00
9 changed files with 130 additions and 37 deletions

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || items.name,\n cte.parent_class_names || item_classes.name\n FROM items\n JOIN cte\n ON items.parent = cte.id\n JOIN item_classes\n ON items.class = item_classes.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\",\n item_states.state AS \"state!: ItemState\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n ORDER BY items.created_at\n ",
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || parent.name,\n cte.parent_class_names || parent_class.name\n FROM cte\n JOIN items\n ON items.parent = cte.id\n JOIN items AS \"parent\"\n ON parent.id = cte.id\n JOIN item_classes AS \"parent_class\"\n ON parent.class = parent_class.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\",\n item_states.state AS \"state!: ItemState\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n ORDER BY items.created_at\n ",
"describe": {
"columns": [
{
@ -70,5 +70,5 @@
true
]
},
"hash": "7a4eab90f0cf7b2d843fe448f14aef9ff6a630b8fde57bb2765b38858b4fada6"
"hash": "3fe94e76159c7911db688854710271e351ca34273dfdb21a7499f588715a91ee"
}

55
Cargo.lock generated
View file

@ -540,6 +540,46 @@ dependencies = [
"inout",
]
[[package]]
name = "clap"
version = "4.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "clap_lex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]]
name = "colorchoice"
version = "1.0.1"
@ -1016,6 +1056,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
@ -1170,6 +1216,7 @@ dependencies = [
"actix-web",
"barcoders",
"base64 0.22.1",
"clap",
"datamatrix",
"enum-iterator",
"env_logger",
@ -2042,7 +2089,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [
"dotenvy",
"either",
"heck",
"heck 0.4.1",
"hex",
"once_cell",
"proc-macro2",
@ -2180,6 +2227,12 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"

View file

@ -15,6 +15,7 @@ actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-web = { version = "4.8.0", features = ["cookies"] }
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
base64 = "0.22.1"
clap = { version = "4.5.10", features = ["derive", "env"] }
datamatrix = "0.3.1"
enum-iterator = "2.1.0"
env_logger = "0.11.3"

34
src/config.rs Normal file
View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::net::{IpAddr, Ipv6Addr};
use clap::Parser;
/// A lightweight inventory management system
#[derive(Clone, Parser, Debug)]
#[command(version, about)]
pub struct Config {
/// Database URL of PostgreSQL database
#[arg(long, env)]
pub database_url: String,
/// Secret key for encrypting session cookie
///
/// Can be generated with head -c 64 /dev/urandom | base64 -w0
#[arg(long, env)]
pub secret_key: Option<String>,
/// Address for HTTP server to listen on
#[arg(long, env, default_value_t = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))]
pub listen_address: std::net::IpAddr,
/// Port for HTTP server to listen on
#[arg(long, env, default_value_t = 8080)]
pub listen_port: u16,
/// Superuser password
#[arg(long, env)]
pub superuser_password: String,
}

View file

@ -59,12 +59,10 @@ async fn login(
req: HttpRequest,
form: web::Form<LoginForm>,
query: web::Query<LoginQuery>,
config: web::Data<crate::Config>,
) -> Result<impl Responder, error::Error> {
// Very basic authentication for now (only password, hardcoded in environment variable)
if form.password
== std::env::var("SUPERUSER_PASSWORD")
.map_err(|_| error::ErrorInternalServerError("login disabled (no password set)"))?
{
// Very basic authentication for now (only password, hardcoded in configuration)
if form.password == config.superuser_password {
Identity::login(&req.extensions(), "superuser".into())
.map_err(error::ErrorInternalServerError)?;
Ok(

View file

@ -47,13 +47,15 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
SELECT
items.id,
cte.parents || items.parent,
cte.parent_names || items.name,
cte.parent_class_names || item_classes.name
FROM items
JOIN cte
cte.parent_names || parent.name,
cte.parent_class_names || parent_class.name
FROM cte
JOIN items
ON items.parent = cte.id
JOIN item_classes
ON items.class = item_classes.id
JOIN items AS "parent"
ON parent.id = cte.id
JOIN item_classes AS "parent_class"
ON parent.class = parent_class.id
)
SELECT
cte.id AS "id!",

View file

@ -117,21 +117,23 @@ pub fn base(config: TemplateConfig, content: Markup) -> Markup {
(navbar(&config))
main .container.my-4 {
div .d-flex.justify-content-between.mb-3 {
div {
div .float-end.d-flex.align-items-start.h-100.gap-1 {
@for page_action in config.page_actions {
(page_action)
}
}
div .d-flex.align-items-center.gap-1 {
@if let Some(ref page_title) = config.page_title {
h2 {
(page_title)
@if let Some(ref page_title_extra) = config.page_title_extra {
" "
(page_title_extra)
}
}
}
@if let Some(ref page_title_extra) = config.page_title_extra {
(page_title_extra)
}
}
div .d-flex.h-100.gap-1 {
@for page_action in config.page_actions {
(page_action)
}
}
}

View file

@ -2,6 +2,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
mod config;
pub mod frontend;
pub mod label;
pub mod middleware;
pub use config::Config;

View file

@ -2,17 +2,18 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::env;
use actix_identity::IdentityMiddleware;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::middleware::ErrorHandlers;
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpResponse, HttpServer};
use base64::prelude::{Engine as _, BASE64_STANDARD};
use clap::Parser;
use log::{info, warn};
use mime_guess::from_path;
use rust_embed::Embed;
use li7y::Config;
#[derive(Embed)]
#[folder = "static"]
struct Static;
@ -21,39 +22,38 @@ struct Static;
async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let config = Config::parse();
// generate a secret key with head -c 64 /dev/urandom | base64 -w0
let secret_key = match env::var("SECRET_KEY") {
Ok(encoded) => Key::from(
let secret_key = match config.secret_key {
Some(ref encoded) => Key::from(
&BASE64_STANDARD
.decode(encoded)
.expect("failed to decode base64 in SECRET_KEY"),
),
Err(_) => {
None => {
warn!("SECRET_KEY was not specified, using randomly generated key");
Key::generate()
}
};
let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect(
&env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
)
.await
.expect("failed to connect to database");
let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect(&config.database_url)
.await
.expect("failed to connect to database");
sqlx::migrate!()
.run(&pool)
.await
.expect("failed to run migrations");
let address = env::var("LISTEN_ADDRESS").unwrap_or("::1".to_string());
let port = env::var("LISTEN_PORT").map_or(8080, |s| {
s.parse::<u16>().expect("failed to parse LISTEN_PORT")
});
let address = config.listen_address;
let port = config.listen_port;
info!("Starting on {address}:{port}");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(config.clone()))
.app_data(web::Data::new(pool.clone()))
.service(web::scope("/static").route(
"/{_:.*}",