Compare commits

..

5 commits

Author SHA1 Message Date
Simon Bruder a78dc85dbf
Add item state
All checks were successful
/ build (push) Successful in 24s
2024-07-24 00:14:51 +02:00
Simon Bruder 1d831585bd
flake: Update pre-commit-hooks.nix to new name
All checks were successful
/ build (push) Successful in 26s
This also removes all input overrides.
2024-07-24 00:14:02 +02:00
Simon Bruder a72d5b40e2
CI: Only gate OCI image push on branch
All checks were successful
/ build (push) Successful in 3m31s
2024-07-24 00:07:56 +02:00
Simon Bruder 1f6298af4a
Remove home link from navbar
There is nothing there and it can also be reached by clicking on the
branding.
2024-07-21 23:46:15 +02:00
Simon Bruder 5b3dd34312
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-21 23:43:53 +02:00
9 changed files with 37 additions and 130 deletions

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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 || 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 ", "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 ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -70,5 +70,5 @@
true true
] ]
}, },
"hash": "3fe94e76159c7911db688854710271e351ca34273dfdb21a7499f588715a91ee" "hash": "7a4eab90f0cf7b2d843fe448f14aef9ff6a630b8fde57bb2765b38858b4fada6"
} }

55
Cargo.lock generated
View file

@ -540,46 +540,6 @@ dependencies = [
"inout", "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]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.1" version = "1.0.1"
@ -1056,12 +1016,6 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -1216,7 +1170,6 @@ dependencies = [
"actix-web", "actix-web",
"barcoders", "barcoders",
"base64 0.22.1", "base64 0.22.1",
"clap",
"datamatrix", "datamatrix",
"enum-iterator", "enum-iterator",
"env_logger", "env_logger",
@ -2089,7 +2042,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
"heck 0.4.1", "heck",
"hex", "hex",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
@ -2227,12 +2180,6 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"

View file

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

View file

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

View file

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

View file

@ -117,22 +117,20 @@ pub fn base(config: TemplateConfig, content: Markup) -> Markup {
(navbar(&config)) (navbar(&config))
main .container.my-4 { main .container.my-4 {
div { div .d-flex.justify-content-between.mb-3 {
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 { div .d-flex.align-items-center.gap-1 {
@if let Some(ref page_title) = config.page_title { @if let Some(ref page_title) = config.page_title {
h2 { h2 {
(page_title) (page_title)
}
}
@if let Some(ref page_title_extra) = config.page_title_extra { @if let Some(ref page_title_extra) = config.page_title_extra {
" "
(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,9 +2,6 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
mod config;
pub mod frontend; pub mod frontend;
pub mod label; pub mod label;
pub mod middleware; pub mod middleware;
pub use config::Config;

View file

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