diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 1970150..3922721 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,10 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -.env \ No newline at end of file +.env + +# direnv / nix +.direnv/ +.envrc.local +result +result-* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 3cde229..3ec9776 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,16 @@ crate-type = ["cdylib", "rlib"] [dependencies] # --- Core Leptos Dependencies --- # Set default-features = false to manually control 'ssr' and 'hydrate' features. -leptos = { version = "0.8.2", features = ["nightly"] } -leptos_router = { version = "0.8.2", features = ["nightly"] } -leptos_meta = { version = "0.8.2" } -leptos-use = { version = "0.16.2" } +leptos = { version = "0.8.17", features = ["nightly"] } +leptos_router = { version = "0.8.12", features = ["nightly"] } +leptos_meta = { version = "0.8.6" } +leptos-use = { version = "0.16.3" } # --- Client-Side (WASM) Dependencies --- # These are only needed for the 'hydrate' feature. console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen = { version = "0.2.100", optional = true } -web-sys = { version = "0.3.77", features = [ +wasm-bindgen = { version = "0.2.114", optional = true } +web-sys = { version = "0.3.91", features = [ "Window", "Document", "Storage", @@ -27,24 +27,28 @@ web-sys = { version = "0.3.77", features = [ # --- Server-Side (Binary) Dependencies --- # These are correctly marked as optional and will be enabled by the 'ssr' feature. -axum = { version = "0.8.4", features = ["ws"], optional = true } -leptos_axum = { version = "0.8.2", optional = true } -tokio = { version = "1.45", features = ["rt-multi-thread", "macros"], optional = true } -tower = { version = "0.5.2", optional = true } -tower-http = { version = "0.6.6", features = ["fs"], optional = true } -surrealdb = { version = "2.3.4", optional = true } +axum = { version = "0.8.8", features = ["ws"], optional = true } +leptos_axum = { version = "0.8.8", optional = true } +tokio = { version = "1.50", features = ["rt-multi-thread", "macros"], optional = true } +tower = { version = "0.5.3", optional = true } +tower-http = { version = "0.6.8", features = ["fs"], optional = true } +diesel = { version = "2.2", features = ["postgres", "uuid", "chrono"], optional = true } +diesel-async = { version = "0.5", features = ["postgres", "deadpool"], optional = true } +diesel_migrations = { version = "2.2", optional = true } +uuid = { version = "1", features = ["v4", "serde"], optional = true } +chrono = { version = "0.4", features = ["serde"], optional = true } dotenvy = { version = "0.15.7", optional = true } tower-sessions = { version = "0.14.0", optional = true } argon2 = { version = "0.5.3", optional = true } -async-trait = { version = "0.1.88", optional = true } +async-trait = { version = "0.1.89", optional = true } time = { version = "0.3", optional = true } tokio-tungstenite = { version = "0.27.0", optional = true } -futures-util = { version = "0.3.31", optional = true } +futures-util = { version = "0.3.32", optional = true } # --- Common Dependencies --- thiserror = "2.0" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" validator = { version = "0.20.0", features = ["derive"] } @@ -66,7 +70,11 @@ ssr = [ "dep:tower", "dep:tower-http", "dep:leptos_axum", - "dep:surrealdb", + "dep:diesel", + "dep:diesel-async", + "dep:diesel_migrations", + "dep:uuid", + "dep:chrono", "dep:dotenvy", "dep:tower-sessions", "dep:argon2", diff --git a/Cargo.toml.prod b/Cargo.toml.prod deleted file mode 100644 index df4ddd3..0000000 --- a/Cargo.toml.prod +++ /dev/null @@ -1,127 +0,0 @@ -[package] -name = "order-stream" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -# --- Core Leptos Dependencies --- -# Set default-features = false to manually control 'ssr' and 'hydrate' features. -leptos = { version = "0.8.2", default-features = false, features = ["nightly"] } -leptos_router = { version = "0.8.2", default-features = false, features = ["nightly"] } -leptos_meta = { version = "0.8.2", default-features = false } -leptos-use = { version = "0.16.2" } - -# --- Client-Side (WASM) Dependencies --- -# These are only needed for the 'hydrate' feature. -console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen = { version = "0.2.100", optional = true } -web-sys = { version = "0.3.77", features = [ - "Window", - "Document", - "Storage", - "MediaQueryList", -] } - -# --- Server-Side (Binary) Dependencies --- -# These are correctly marked as optional and will be enabled by the 'ssr' feature. -axum = { version = "0.8.4", features = ["ws"], optional = true } -leptos_axum = { version = "0.8.2", optional = true } -tokio = { version = "1.45", features = ["rt-multi-thread", "macros"], optional = true } -tower = { version = "0.5.2", optional = true } -tower-http = { version = "0.6.6", features = ["fs"], optional = true } -surrealdb = { version = "2.3.4", optional = true } -dotenvy = { version = "0.15.7", optional = true } -tower-sessions = { version = "0.14.0", optional = true } -argon2 = { version = "0.5.3", optional = true } -async-trait = { version = "0.1.88", optional = true } -time = { version = "0.3", optional = true } -tokio-tungstenite = { version = "0.27.0", optional = true } -futures-util = { version = "0.3.31", optional = true } - -# --- Common Dependencies --- -thiserror = "2.0" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -validator = { version = "0.20.0", features = ["derive"] } - - -[features] -# --- Hydrate Feature: For client-side WASM compilation --- -hydrate = [ - "leptos/hydrate", - "dep:console_error_panic_hook", - "dep:wasm-bindgen", -] - -# --- SSR Feature: For server-side binary compilation --- -ssr = [ - "leptos/ssr", - "leptos_meta/ssr", - "leptos_router/ssr", - "dep:axum", - "dep:tokio", - "dep:tower", - "dep:tower-http", - "dep:leptos_axum", - "dep:surrealdb", - "dep:dotenvy", - "dep:tower-sessions", - "dep:argon2", - "dep:async-trait", - "dep:time", - "dep:tokio-tungstenite", - "dep:futures-util", -] - -[package.metadata.cargo-all-features] -denylist = ["axum", "tokio", "tower", "tower-http", "leptos_axum"] -skip_feature_sets = [["ssr", "hydrate"], []] - -[package.metadata.leptos] -# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. -output-name = "order-stream" -# The site root folder is where cargo-leptos generates all output. -site-root = "target/site" -# The site-root relative folder where all compiled output (JS, WASM and CSS) is written -site-pkg-dir = "pkg" -# The tailwind input file. -tailwind-input-file = "input.css" -# Files in the asset-dir will be copied to the site-root directory -assets-dir = "public" -# The IP and port where the server serves the content. -site-addr = "127.0.0.1:3000" -# The port to use for automatic reload monitoring -reload-port = 3001 -# Command to use when running end2end tests. -end2end-cmd = "npx playwright test" -end2end-dir = "end2end" -# The browserlist query used for optimizing the CSS. -browserquery = "defaults" -# Controls whether autoreload JS is included in the head -watch = false - -# --- IMPORTANT PRODUCTION CHANGE --- -# Set the environment to "PROD" to disable devtools and other development-only features. -env = "PROD" - -# The features to use when compiling the bin target -bin-features = ["ssr"] -# If the --no-default-features flag should be used when compiling the bin target -bin-default-features = false - -# The features to use when compiling the lib target -lib-features = ["hydrate"] -# If the --no-default-features flag should be used when compiling the lib target -lib-default-features = false - -# --- PRODUCTION BUILD OPTIMIZATIONS --- -# These settings tell the Rust compiler to optimize for a small and fast binary. -[profile.release] -opt-level = 'z' # Optimize for size. -lto = "fat" # Enable Link Time Optimization. -codegen-units = 1 # Reduce parallel code generation units for better optimization. -strip = true # Strip debug symbols from the binary. -panic = "abort" # Abort on panic for a smaller binary. diff --git a/README.md b/README.md index c484684..d880461 100644 --- a/README.md +++ b/README.md @@ -92,15 +92,16 @@ The system keeps everyone synchronized: ### Prerequisites -Install [Rust](https://www.rust-lang.org/tools/install), [Leptos](https://book.leptos.dev/getting_started/) + [TailwindCLI](https://tailwindcss.com/docs/installation/tailwind-cli) and [SurrealDB](https://surrealdb.com/install) as Database - -create .env file with Database Connection info in project root: -```Example env -SURREAL_URL=127.0.0.1:8000 -SURREAL_USER=root -SURREAL_PASS=root -SURREAL_DB=orderstream -SURREAL_NS=dev +Install [Rust](https://www.rust-lang.org/tools/install) (nightly), [cargo-leptos](https://book.leptos.dev/getting_started/) + [TailwindCLI](https://tailwindcss.com/docs/installation/tailwind-cli). + +For the database, start PostgreSQL via Docker Compose: +```bash +docker compose up -d +``` + +Alternatively, create a `.env` file with your own PostgreSQL connection: +```env +DATABASE_URL=postgres://user:password@localhost:5432/my_database ``` ## Development usage diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..b674a92 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,2 @@ +[print_schema] +file = "src/backend/schema.rs" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..0026ced --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + db: + image: postgres:15-alpine + container_name: mem_postgres + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: my_database + ports: + - "5432:5432" + # These flags maximize performance for dev/test environments + command: > + postgres + -c fsync=off + -c full_page_writes=off + -c synchronous_commit=off + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..197ba28 --- /dev/null +++ b/flake.lock @@ -0,0 +1,100 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1777708550, + "narHash": "sha256-Qif3UXT0l5OQq8H9pRWt4/ia4gF48MWK2oHKL8uVx8U=", + "owner": "nix-community", + "repo": "fenix", + "rev": "74c1591efaff494756b8d35ebe357c6c2bbdca96", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777578337, + "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1777639980, + "narHash": "sha256-6d7Hdurvbjc5uwJuc0YiK7rZBGj6Gs3uzfBFcTs+xCc=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "64cdaeb06f69b6b769a492edd88b022ae88e8ca2", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a041dfb --- /dev/null +++ b/flake.nix @@ -0,0 +1,71 @@ +{ + description = "OrderStream — Leptos + Axum + Postgres dev shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, fenix, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + rustToolchain = with fenix.packages.${system}; combine [ + complete.cargo + complete.rustc + complete.rust-src + complete.rustfmt + complete.clippy + complete.rust-analyzer + targets.wasm32-unknown-unknown.latest.rust-std + ]; + + dieselCli = pkgs.diesel-cli.override { + sqliteSupport = false; + mysqlSupport = false; + postgresqlSupport = true; + }; + + wasmBindgenVersion = "0.2.120"; + in + { + devShells.default = pkgs.mkShell { + packages = [ + rustToolchain + pkgs.cargo-leptos + pkgs.cargo-binstall + pkgs.tailwindcss_4 + dieselCli + pkgs.docker-compose + pkgs.pkg-config + pkgs.openssl + pkgs.postgresql.lib + ]; + + env.PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + + env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.postgresql.lib + pkgs.openssl + ]; + + shellHook = '' + export PATH="$HOME/.cargo/bin:$PATH" + need_install=1 + if command -v wasm-bindgen >/dev/null 2>&1; then + current="$(wasm-bindgen --version 2>/dev/null | awk '{print $2}')" + [ "$current" = "${wasmBindgenVersion}" ] && need_install=0 + fi + if [ "$need_install" = 1 ]; then + echo "Installing wasm-bindgen-cli ${wasmBindgenVersion} via cargo-binstall..." + cargo binstall -y --force --version ${wasmBindgenVersion} wasm-bindgen-cli + fi + ''; + }; + }); +} diff --git a/migrations/.diesel_lock b/migrations/.diesel_lock new file mode 100644 index 0000000..e69de29 diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2026-03-12-163145-0000_initial_schema/down.sql b/migrations/2026-03-12-163145-0000_initial_schema/down.sql new file mode 100644 index 0000000..e7f81ed --- /dev/null +++ b/migrations/2026-03-12-163145-0000_initial_schema/down.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS settings; +DROP TABLE IF EXISTS station_input_statuses; +DROP TABLE IF EXISTS station_categories; +DROP TABLE IF EXISTS stations; +DROP TABLE IF EXISTS items; +DROP TABLE IF EXISTS orders; +DROP TABLE IF EXISTS products; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS categories; +DROP TABLE IF EXISTS users; +DROP EXTENSION IF EXISTS "uuid-ossp"; diff --git a/migrations/2026-03-12-163145-0000_initial_schema/up.sql b/migrations/2026-03-12-163145-0000_initial_schema/up.sql new file mode 100644 index 0000000..fd5ffc3 --- /dev/null +++ b/migrations/2026-03-12-163145-0000_initial_schema/up.sql @@ -0,0 +1,91 @@ +-- Enable uuid generation +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Users (UUID pk for security) +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'Visitor' +); + +-- Categories +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE +); + +-- Events +CREATE TABLE events ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL +); + +-- Products +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE RESTRICT, + price DOUBLE PRECISION NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Orders +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE RESTRICT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Items (order line items) +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT, + quantity INTEGER NOT NULL, + price DOUBLE PRECISION NOT NULL, + status TEXT NOT NULL DEFAULT 'Ordered' +); + +-- Stations +CREATE TABLE stations ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE, + output_status TEXT NOT NULL +); + +-- Station <-> Category junction (which categories this station handles) +CREATE TABLE station_categories ( + station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE, + category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE, + PRIMARY KEY (station_id, category_id) +); + +-- Station input statuses junction (which item statuses feed into this station) +CREATE TABLE station_input_statuses ( + station_id INTEGER NOT NULL REFERENCES stations(id) ON DELETE CASCADE, + status TEXT NOT NULL, + PRIMARY KEY (station_id, status) +); + +-- Settings (singleton row) +CREATE TABLE settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + active_event_id INTEGER REFERENCES events(id) ON DELETE SET NULL, + CONSTRAINT single_row CHECK (id = 1) +); + +INSERT INTO settings (id, active_event_id) VALUES (1, NULL); + +-- Sessions +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expiry_date TIMESTAMPTZ NOT NULL +); + +-- Indexes +CREATE INDEX idx_products_category ON products(category_id); +CREATE INDEX idx_items_order ON items(order_id); +CREATE INDEX idx_items_status ON items(status); +CREATE INDEX idx_sessions_expiry ON sessions(expiry_date); diff --git a/src/app/components/cashier/order.rs b/src/app/components/cashier/order.rs index 3c7da69..77a6654 100644 --- a/src/app/components/cashier/order.rs +++ b/src/app/components/cashier/order.rs @@ -127,7 +127,7 @@ pub fn Order() -> impl IntoView { .enumerate() .map(|(index, item)| types::Item { id: format!("temp_{}", index), - order_id: None, + order_id: String::new(), product_id: item.product_id.clone(), quantity: item.quantity, price: item.price, diff --git a/src/app/components/station_view.rs b/src/app/components/station_view.rs index 443c44c..0b5f9a1 100644 --- a/src/app/components/station_view.rs +++ b/src/app/components/station_view.rs @@ -221,9 +221,7 @@ pub fn StationView(station_id: String) -> impl IntoView { // Group items by order_id let mut orders: HashMap> = HashMap::new(); for item in items { - if let Some(order_id) = &item.order_id { - orders.entry(order_id.clone()).or_insert_with(Vec::new).push(item); - } + orders.entry(item.order_id.clone()).or_insert_with(Vec::new).push(item); } if orders.is_empty() { diff --git a/src/app/pages/signin.rs b/src/app/pages/signin.rs index 1b699cc..c58be1a 100644 --- a/src/app/pages/signin.rs +++ b/src/app/pages/signin.rs @@ -1,6 +1,8 @@ use leptos::prelude::*; +#[cfg(feature = "hydrate")] use leptos_router::hooks::use_navigate; use crate::backend::user::Login; +#[cfg(feature = "hydrate")] use crate::app::states::user; use crate::app::components::atoms::icons; @@ -8,7 +10,8 @@ use crate::app::components::atoms::icons; pub fn SignIn() -> impl IntoView { let login_action = ServerAction::::new(); - // Handle successful login + // Handle successful login (client-side only to avoid SendWrapper panic) + #[cfg(feature = "hydrate")] Effect::new(move |_| { if let Some(Ok(user)) = login_action.value().get() { let user_state = user::get(); diff --git a/src/app/pages/signup.rs b/src/app/pages/signup.rs index 2f077ab..6941300 100644 --- a/src/app/pages/signup.rs +++ b/src/app/pages/signup.rs @@ -1,6 +1,8 @@ use leptos::prelude::*; +#[cfg(feature = "hydrate")] use leptos_router::hooks::use_navigate; use crate::backend::user::CreateUser; +#[cfg(feature = "hydrate")] use crate::app::states::user; use crate::app::components::atoms::icons; @@ -8,7 +10,8 @@ use crate::app::components::atoms::icons; pub fn SignUp() -> impl IntoView { let signup_action = ServerAction::::new(); - // Handle successful signup + // Handle successful signup (client-side only to avoid SendWrapper panic) + #[cfg(feature = "hydrate")] Effect::new(move |_| { if let Some(Ok(user)) = signup_action.value().get() { let user_state = user::get(); diff --git a/src/app/states/user.rs b/src/app/states/user.rs index 0ad7d95..b385229 100644 --- a/src/app/states/user.rs +++ b/src/app/states/user.rs @@ -14,22 +14,9 @@ pub struct UserState { impl UserState { pub fn new() -> Self { let user: RwSignal> = RwSignal::new(None); - let loading: RwSignal = RwSignal::new(true); // Start as loading + let loading: RwSignal = RwSignal::new(true); let (users, set_users) = signal(Vec::new()); - // Load users once on initialization - Effect::new({ - let set_users = set_users; - move |_| { - spawn_local(async move { - match get_all_users().await { - Ok(user_list) => set_users.set(user_list), - Err(_) => {}, // Keep empty vec on error - } - }); - } - }); - Self { user, loading, users, set_users } } diff --git a/src/backend/auth.rs b/src/backend/auth.rs index a68e47d..9116bcc 100644 --- a/src/backend/auth.rs +++ b/src/backend/auth.rs @@ -1,18 +1,20 @@ -use crate::backend::db::DB; +use crate::backend::db::get_pool; use crate::common::errors::Error; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use async_trait::async_trait; +use chrono::{Duration, Utc}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use surrealdb::RecordId; use time::OffsetDateTime; use tower_sessions::{ session::{Id, Record}, session_store, SessionStore, }; +use uuid::Uuid; pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); @@ -55,6 +57,10 @@ pub fn get_extended_expiry() -> OffsetDateTime { OffsetDateTime::now_utc() + time::Duration::hours(24) } +fn get_extended_expiry_chrono() -> chrono::DateTime { + Utc::now() + Duration::hours(24) +} + #[cfg(feature = "ssr")] pub fn handle_session_extension(session: &tower_sessions::Session) { if let Some(expiry) = session.expiry() { @@ -68,36 +74,37 @@ pub fn handle_session_extension(session: &tower_sessions::Session) { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SessionRecord { - id: Option, - session_id: String, - data: HashMap, -} - #[derive(Debug, Clone)] -pub struct SurrealSessionStore; +pub struct PostgresSessionStore; -impl SurrealSessionStore { +impl PostgresSessionStore { pub fn new() -> Self { Self } } -const SESSIONS_TABLE: &str = "sessions"; - #[async_trait] -impl SessionStore for SurrealSessionStore { +impl SessionStore for PostgresSessionStore { async fn create(&self, record: &mut Record) -> session_store::Result<()> { - let session_record = SessionRecord { - id: None, - session_id: record.id.to_string(), - data: record.data.clone(), + use crate::backend::schema::sessions::dsl::*; + use crate::backend::models::NewSession; + + let user_id_val: Uuid = extract_user_id(record) + .map_err(|e| session_store::Error::Backend(e))?; + + let new_session = NewSession { + id: record.id.to_string(), + user_id: user_id_val, + expiry_date: get_extended_expiry_chrono(), }; - let _: Option = DB - .create((SESSIONS_TABLE, &record.id.to_string())) - .content(session_record) + let pool = get_pool(); + let mut conn = pool.get().await + .map_err(|e| session_store::Error::Backend(e.to_string()))?; + + diesel::insert_into(sessions) + .values(&new_session) + .execute(&mut conn) .await .map_err(|e| session_store::Error::Backend(e.to_string()))?; @@ -105,15 +112,21 @@ impl SessionStore for SurrealSessionStore { } async fn save(&self, record: &Record) -> session_store::Result<()> { - let session_record = SessionRecord { - id: None, - session_id: record.id.to_string(), - data: record.data.clone(), - }; + use crate::backend::schema::sessions::dsl::*; + + let user_id_val: Uuid = extract_user_id(record) + .map_err(|e| session_store::Error::Backend(e))?; - let _: Option = DB - .update((SESSIONS_TABLE, &record.id.to_string())) - .content(session_record) + let pool = get_pool(); + let mut conn = pool.get().await + .map_err(|e| session_store::Error::Backend(e.to_string()))?; + + diesel::update(sessions.filter(id.eq(record.id.to_string()))) + .set(( + user_id.eq(user_id_val), + expiry_date.eq(get_extended_expiry_chrono()), + )) + .execute(&mut conn) .await .map_err(|e| session_store::Error::Backend(e.to_string()))?; @@ -121,53 +134,109 @@ impl SessionStore for SurrealSessionStore { } async fn load(&self, session_id: &Id) -> session_store::Result> { - let session_record: Option = DB - .select((SESSIONS_TABLE, &session_id.to_string())) + use crate::backend::schema::sessions::dsl::*; + use crate::backend::models::DbSession; + + let pool = get_pool(); + let mut conn = pool.get().await + .map_err(|e| session_store::Error::Backend(e.to_string()))?; + + let session: Option = sessions + .filter(id.eq(session_id.to_string())) + .filter(expiry_date.gt(diesel::dsl::now)) + .select(DbSession::as_select()) + .first(&mut conn) .await + .optional() .map_err(|e| session_store::Error::Backend(e.to_string()))?; - match session_record { - Some(record) => Ok(Some(Record { - id: session_id.clone(), - data: record.data, - expiry_date: OffsetDateTime::now_utc() + time::Duration::days(30), - })), + match session { + Some(s) => { + let mut data = std::collections::HashMap::new(); + let session_data = SessionData::new(s.user_id.to_string()); + data.insert( + "user".to_string(), + serde_json::to_value(session_data) + .map_err(|e| session_store::Error::Backend(e.to_string()))?, + ); + + let expiry = OffsetDateTime::from_unix_timestamp(s.expiry_date.timestamp()) + .unwrap_or_else(|_| get_extended_expiry()); + + Ok(Some(Record { + id: session_id.clone(), + data, + expiry_date: expiry, + })) + } None => Ok(None), } } async fn delete(&self, session_id: &Id) -> session_store::Result<()> { - let deleted: Option = DB - .delete((SESSIONS_TABLE, &session_id.to_string())) + use crate::backend::schema::sessions::dsl::*; + + let pool = get_pool(); + let mut conn = pool.get().await + .map_err(|e| session_store::Error::Backend(e.to_string()))?; + + diesel::delete(sessions.filter(id.eq(session_id.to_string()))) + .execute(&mut conn) .await .map_err(|e| session_store::Error::Backend(e.to_string()))?; - if deleted.is_none() { - return Err(session_store::Error::Backend( - "Could not delete Session.".into(), - )); - } + Ok(()) } } +fn extract_user_id(record: &Record) -> Result { + let session_data: SessionData = record + .data + .get("user") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .ok_or_else(|| "Missing user session data".to_string())?; + + Uuid::parse_str(&session_data.user_id) + .map_err(|e| format!("Invalid user UUID in session: {}", e)) +} + pub async fn get_authenticated_user( session: &tower_sessions::Session, ) -> Result { - use crate::backend::user::ssr::{User, USERS}; - let session_data: Option = session.get("user").await?; + use crate::backend::models::{DbUser, ParseRole}; + use crate::backend::schema::users::dsl::*; + let session_data: Option = session.get("user").await?; let session_data = session_data.ok_or_else(|| Error::NotAuthorized("Not authenticated".to_string()))?; handle_session_extension(session); - let user: Option = DB.select((USERS, &session_data.user_id)).await?; + let user_uuid = Uuid::parse_str(&session_data.user_id) + .map_err(|_| Error::NotAuthorized("Invalid session".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let user: Option = users + .filter(id.eq(user_uuid)) + .select(DbUser::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; let user = user.ok_or_else(|| { let _ = session.delete(); Error::NotAuthorized("User not found".to_string()) })?; - Ok(user.into()) + + Ok(crate::common::types::User { + id: user.id.to_string(), + email: user.email, + role: user.role.parse_role(), + }) } #[macro_export] diff --git a/src/backend/category.rs b/src/backend/category.rs index 13febc2..9b24d8f 100644 --- a/src/backend/category.rs +++ b/src/backend/category.rs @@ -3,108 +3,132 @@ use leptos::prelude::*; use crate::common::{requests, types}; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::backend::websocket::{broadcast_add, broadcast_delete, broadcast_update}; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; - - pub const CATEGORIES: &str = "categories"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Category { - pub id: Option, - #[validate(length(min = 1, max = 64))] - pub name: String, - } - - impl From for types::Category { - fn from(record: Category) -> Self { - Self { - id: record.id.unwrap().key().to_string(), - name: record.name, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; +use crate::common::errors::Error; #[server(CreateCategory, "/api/category")] pub async fn create_category( req: requests::category::Create, ) -> Result { - let c: Option = DB - .create(CATEGORIES) - .content(Category { - id: None, - name: req.name, - }) - .await?; - - if let Some(category) = c { - let result: types::Category = category.clone().into(); - broadcast_add(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to create category".into())) - } + use crate::backend::db::get_pool; + use crate::backend::models::{DbCategory, NewCategory}; + use crate::backend::websocket::{broadcast_add}; + use crate::backend::schema::categories::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let category: DbCategory = diesel::insert_into(categories) + .values(NewCategory { name: &req.name }) + .returning(DbCategory::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create category: {}", e)))?; + + let result: types::Category = category.into(); + broadcast_add(result.clone()); + Ok(result) } #[server(GetCategories, "/api/category")] pub async fn get_categories() -> Result, ServerFnError> { - let categories: Vec = DB.select(CATEGORIES).await?; - Ok(categories.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbCategory; + use crate::backend::schema::categories::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = categories + .select(DbCategory::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetCategory, "/api/category")] -pub async fn get_category(id: String) -> Result { - let category: Option = DB.select((CATEGORIES, &id)).await?; - category - .map(Into::into) - .ok_or_else(|| ServerError("Category not found".into())) +pub async fn get_category(category_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbCategory; + use crate::backend::schema::categories::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let cid: i32 = category_id.parse() + .map_err(|_| Error::InternalError("Invalid category ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let category: Option = categories + .filter(id.eq(cid)) + .select(DbCategory::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + category.map(Into::into) + .ok_or_else(|| Error::InternalError("Category not found".to_string()).into()) } #[server(UpdateCategory, "/api/category")] pub async fn update_category( - id: String, + category_id: String, update: requests::category::Update, ) -> Result { - // Get the existing category - let existing_category: Option = DB.select((CATEGORIES, &id)).await?; - if existing_category.is_none() { - return Err(ServerError("Category not found".into())); - } - let category = existing_category.unwrap(); - let updated = Category { - id: category.id, - name: update.name.or_else(|| Some(category.name)).unwrap(), - }; - // Update the category in the database - let updated_category: Option = DB - .update((CATEGORIES, &id)) - .content(updated) - .await?; - - if let Some(category) = updated_category { - let result: types::Category = category.into(); - broadcast_update(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to update category".into())) - } + use crate::backend::db::get_pool; + use crate::backend::models::{DbCategory, UpdateCategory}; + use crate::backend::websocket::broadcast_update; + use crate::backend::schema::categories::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let cid: i32 = category_id.parse() + .map_err(|_| Error::InternalError("Invalid category ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let category: DbCategory = diesel::update(categories.filter(id.eq(cid))) + .set(UpdateCategory { name: update.name }) + .returning(DbCategory::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update category: {}", e)))?; + + let result: types::Category = category.into(); + broadcast_update(result.clone()); + Ok(result) } #[server(DeleteCategory, "/api/category")] -pub async fn delete_category(id: String) -> Result<(), ServerFnError> { - let deleted: Option = DB.delete((CATEGORIES, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("Category with id {} not found", id))); +pub async fn delete_category(category_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::websocket::broadcast_delete; + use crate::backend::schema::categories::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let cid: i32 = category_id.parse() + .map_err(|_| Error::InternalError("Invalid category ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(categories.filter(id.eq(cid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Category {} not found", category_id)).into()); } - broadcast_delete::(id); + broadcast_delete::(category_id); Ok(()) } diff --git a/src/backend/config.rs b/src/backend/config.rs index e843549..6acca17 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -1,32 +1,17 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseConfig { - pub url: String, - pub user: String, - pub pass: String, - pub db: String, - pub ns: String, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { - pub database: DatabaseConfig, + pub database_url: String, } impl AppConfig { pub fn from_env() -> Result> { - dotenvy::dotenv().ok(); // Load .env file if it exists + dotenvy::dotenv().ok(); - let database = DatabaseConfig { - url: std::env::var("SURREAL_URL") - .unwrap_or_else(|_| "127.0.0.1:8000".to_string()), - user: std::env::var("SURREAL_USER").unwrap_or_else(|_| "root".to_string()), - pass: std::env::var("SURREAL_PASS").unwrap_or_else(|_| "root".to_string()), - db: std::env::var("SURREAL_DB").unwrap_or_else(|_| "orderstream".to_string()), - ns: std::env::var("SURREAL_NS").unwrap_or_else(|_| "dev".to_string()), - }; + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://user:password@localhost:5432/my_database".to_string()); - Ok(AppConfig { database }) + Ok(AppConfig { database_url }) } } diff --git a/src/backend/db.rs b/src/backend/db.rs index 593468d..8a7882a 100644 --- a/src/backend/db.rs +++ b/src/backend/db.rs @@ -1,32 +1,42 @@ -use std::sync::LazyLock; -use surrealdb::engine::remote::ws::{Client, Ws}; -use surrealdb::opt::auth::Root; -use surrealdb::Surreal; +use diesel_async::{AsyncPgConnection, pooled_connection::deadpool::Pool, pooled_connection::AsyncDieselConnectionManager}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use diesel::pg::PgConnection; +use diesel::Connection; +use std::sync::OnceLock; -pub type Database = Surreal; +pub type DbPool = Pool; -pub static DB: LazyLock> = LazyLock::new(Surreal::init); +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + +static POOL: OnceLock = OnceLock::new(); pub async fn initialize_database() -> Result<(), String> { use crate::backend::config::AppConfig; let config = AppConfig::from_env().map_err(|e| format!("Could not load env: {}", e))?; - DB.connect::(&config.database.url) - .await - .map_err(|e| format!("Failed to connect to database: {}", e))?; + // Run migrations synchronously (diesel_migrations requires a sync connection) + let mut sync_conn = PgConnection::establish(&config.database_url) + .map_err(|e| format!("Failed to connect for migrations: {}", e))?; + sync_conn.run_pending_migrations(MIGRATIONS) + .map_err(|e| format!("Failed to run migrations: {}", e))?; - DB.signin(Root { - username: &config.database.user, - password: &config.database.pass, - }) - .await - .map_err(|e| format!("Failed to sign in: {}", e))?; + // Build async connection pool + let manager = AsyncDieselConnectionManager::::new(&config.database_url); + let pool = Pool::builder(manager) + .build() + .map_err(|e| format!("Failed to build connection pool: {}", e))?; - DB.use_ns(&config.database.ns) - .use_db(&config.database.db) + // Verify connectivity + let _ = pool.get() .await - .map_err(|e| format!("Failed to select namespace/database: {}", e))?; + .map_err(|e| format!("Failed to connect to database: {}", e))?; + + POOL.set(pool).map_err(|_| "Pool already initialized".to_string())?; Ok(()) } + +pub fn get_pool() -> &'static DbPool { + POOL.get().expect("Database pool not initialized") +} diff --git a/src/backend/event.rs b/src/backend/event.rs index 3b2adfe..db30faa 100644 --- a/src/backend/event.rs +++ b/src/backend/event.rs @@ -3,104 +3,130 @@ use leptos::prelude::*; use crate::common::{requests, types}; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::backend::websocket::{broadcast_add, broadcast_delete, broadcast_update}; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; - pub const EVENTS: &str = "events"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Event { - pub id: Option, - #[validate(length(min = 1, max = 64))] - pub name: String, - } - impl From for types::Event { - fn from(event: Event) -> Self { - Self { - id: event.id.unwrap().key().to_string(), - name: event.name, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; +use crate::common::errors::Error; #[server(CreateEvent, "/api/event")] pub async fn create_event(req: requests::event::Create) -> Result { - let e: Option = DB.create(EVENTS) - .content(Event { - id: None, - name: req.name, - }) - .await?; - - if let Some(event) = e { - let result: types::Event = event.clone().into(); - broadcast_add(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to create event".into())) - } + use crate::backend::db::get_pool; + use crate::backend::models::{DbEvent, NewEvent}; + use crate::backend::websocket::broadcast_add; + use crate::backend::schema::events::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let event: DbEvent = diesel::insert_into(events) + .values(NewEvent { name: &req.name }) + .returning(DbEvent::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create event: {}", e)))?; + + let result: types::Event = event.into(); + broadcast_add(result.clone()); + Ok(result) } #[server(GetEvents, "/api/event")] pub async fn get_events() -> Result, ServerFnError> { - let events: Vec = DB.select(EVENTS).await?; - Ok(events.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbEvent; + use crate::backend::schema::events::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = events + .select(DbEvent::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetEvent, "/api/event")] -pub async fn get_event(id: String) -> Result { - let event: Option = DB.select((EVENTS, &id)).await?; - event - .map(Into::into) - .ok_or_else(|| ServerError("Event not found".into())) +pub async fn get_event(event_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbEvent; + use crate::backend::schema::events::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let eid: i32 = event_id.parse() + .map_err(|_| Error::InternalError("Invalid event ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let event: Option = events + .filter(id.eq(eid)) + .select(DbEvent::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + event.map(Into::into) + .ok_or_else(|| Error::InternalError("Event not found".to_string()).into()) } #[server(UpdateEvent, "/api/event")] pub async fn update_event( - id: String, + event_id: String, update: requests::event::Update, ) -> Result { - // Get the existing event - let existing_event: Option = DB.select((EVENTS, &id)).await?; - if existing_event.is_none() { - return Err(ServerError("Event not found".into())); - } - let event = existing_event.unwrap(); - let updated = Event { - id: event.id, - name: update.name.or_else(|| Some(event.name)).unwrap(), - }; - // Update the event in the database - let updated_event: Option = DB - .update((EVENTS, &id)) - .content(updated) - .await?; - - if let Some(event) = updated_event { - let result: types::Event = event.clone().into(); - broadcast_update(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to update event".into())) - } + use crate::backend::db::get_pool; + use crate::backend::models::{DbEvent, UpdateEvent}; + use crate::backend::websocket::broadcast_update; + use crate::backend::schema::events::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let eid: i32 = event_id.parse() + .map_err(|_| Error::InternalError("Invalid event ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let event: DbEvent = diesel::update(events.filter(id.eq(eid))) + .set(UpdateEvent { name: update.name }) + .returning(DbEvent::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update event: {}", e)))?; + + let result: types::Event = event.into(); + broadcast_update(result.clone()); + Ok(result) } #[server(DeleteEvent, "/api/event")] -pub async fn delete_event(id: String) -> Result<(), ServerFnError> { - let deleted: Option = DB.delete((EVENTS, &id)).await?; - if let Some(_event) = deleted { - broadcast_delete::(id); - Ok(()) - } else { - Err(ServerError(format!("Event with id {} not found", id))) +pub async fn delete_event(event_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::websocket::broadcast_delete; + use crate::backend::schema::events::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let eid: i32 = event_id.parse() + .map_err(|_| Error::InternalError("Invalid event ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(events.filter(id.eq(eid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Event {} not found", event_id)).into()); } + broadcast_delete::(event_id); + Ok(()) } diff --git a/src/backend/item.rs b/src/backend/item.rs index d554b17..8c0de6f 100644 --- a/src/backend/item.rs +++ b/src/backend/item.rs @@ -3,245 +3,337 @@ use leptos::prelude::*; use crate::common::{requests, types}; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; - pub const ITEMS: &str = "items"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Item { - pub id: Option, - pub order_id: String, - pub product_id: String, - #[validate(range(min = 1))] - pub quantity: u32, - #[validate(range(min = 0.0))] - pub price: f64, - pub status: types::OrderStatus, - } +use crate::common::errors::Error; - impl From for types::Item { - fn from(record: Item) -> Self { - Self { - id: record.id.unwrap().key().to_string(), - order_id: Some(record.order_id), - product_id: record.product_id, - quantity: record.quantity, - price: record.price, - status: record.status, - } - } - } +#[cfg(feature = "ssr")] +pub async fn create_items( + order_id_str: String, + item_list: Vec, +) -> Result, ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::models::{DbItem, NewItem}; + use crate::backend::schema::items::dsl::items as items_table; + use crate::backend::websocket::broadcast_add; + use crate::backend::models::order_status_str; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; - pub async fn create_items(order_id: String, items: Vec) -> Result, leptos::prelude::ServerFnError> { - use crate::backend::product::get_product; - use crate::backend::websocket::broadcast_add; - let mut created_items = Vec::new(); - - for item in items { - // Get the product to fetch the current price - let product = get_product(item.product_id.clone()).await?; - - let db_item: Option = DB.create(ITEMS) - .content(Item { - id: None, - order_id: order_id.clone(), - product_id: item.product_id, - quantity: item.quantity, - price: product.price, // Use current product price - status: types::OrderStatus::Ordered, - }) - .await?; - - if let Some(created) = db_item { - let item_type: types::Item = created.clone().into(); - - // Broadcast the new item - broadcast_add(item_type.clone()); - - created_items.push(item_type); - } else { - return Err(ServerError("Failed to create item".into())); - } - } - - Ok(created_items) + let oid: i32 = order_id_str.parse() + .map_err(|_| Error::InternalError("Invalid order ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let mut created_items = Vec::new(); + for item in item_list { + let pid: i32 = item.product_id.parse() + .map_err(|_| Error::InternalError("Invalid product ID".to_string()))?; + + let product = crate::backend::product::get_product(item.product_id.clone()).await?; + + let new_item = NewItem { + order_id: oid, + product_id: pid, + quantity: item.quantity as i32, + price: product.price, + status: order_status_str(types::OrderStatus::Ordered).to_string(), + }; + + let db_item: DbItem = diesel::insert_into(items_table) + .values(&new_item) + .returning(DbItem::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create item: {}", e)))?; + + let item_type: types::Item = db_item.into(); + broadcast_add(item_type.clone()); + created_items.push(item_type); } + Ok(created_items) } -#[cfg(feature = "ssr")] -use ssr::*; - #[server(GetItemsByOrder, "/api/item")] pub async fn get_items_by_order(order_id: String) -> Result, ServerFnError> { - let query = "SELECT * FROM items WHERE order_id = $order_id"; - let mut response = DB.query(query).bind(("order_id", order_id)).await?; + use crate::backend::db::get_pool; + use crate::backend::models::DbItem; + use crate::backend::schema::items::dsl::{self, items}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; - let items: Vec = response.take(0)?; - Ok(items.into_iter().map(Into::into).collect()) + let oid: i32 = order_id.parse() + .map_err(|_| Error::InternalError("Invalid order ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = items + .filter(dsl::order_id.eq(oid)) + .select(DbItem::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetItems, "/api/item")] pub async fn get_items() -> Result, ServerFnError> { - let items: Vec = DB.select(ITEMS).await?; - Ok(items.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbItem; + use crate::backend::schema::items::dsl::items; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = items + .select(DbItem::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetItem, "/api/item")] -pub async fn get_item(id: String) -> Result { - let item: Option = DB.select((ITEMS, &id)).await?; - item - .map(Into::into) - .ok_or_else(|| ServerError("Item not found".into())) +pub async fn get_item(item_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbItem; + use crate::backend::schema::items::dsl::{self, items}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let iid: i32 = item_id.parse() + .map_err(|_| Error::InternalError("Invalid item ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let item: Option = items + .filter(dsl::id.eq(iid)) + .select(DbItem::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + item.map(Into::into) + .ok_or_else(|| Error::InternalError("Item not found".to_string()).into()) } #[server(UpdateItem, "/api/item")] pub async fn update_item( - id: String, + item_id: String, update: requests::item::Update, ) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::{DbItem, UpdateItem, order_status_str}; use crate::backend::websocket::broadcast_update; - - // Get the existing item - let existing_item: Option = DB.select((ITEMS, &id)).await?; - if existing_item.is_none() { - return Err(ServerError("Item not found".into())); - } - let item = existing_item.unwrap(); - - // If product_id is being changed, get the new price - let new_price = if let Some(ref new_product_id) = update.product_id { - if new_product_id != &item.product_id { - use crate::backend::product::get_product; - let product = get_product(new_product_id.clone()).await?; - product.price - } else { - item.price - } + use crate::backend::schema::items::dsl::{self, items}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let iid: i32 = item_id.parse() + .map_err(|_| Error::InternalError("Invalid item ID".to_string()))?; + + let pid: Option = update.product_id + .as_ref() + .map(|s| s.parse().map_err(|_| Error::InternalError("Invalid product ID".to_string()))) + .transpose()?; + + // If product is changing, look up the new price + let new_price = if let Some(ref new_pid) = update.product_id { + let product = crate::backend::product::get_product(new_pid.clone()).await?; + Some(product.price) } else { - item.price + None }; - - let updated = Item { - id: item.id, - order_id: item.order_id, - product_id: update.product_id.or_else(|| Some(item.product_id)).unwrap(), - quantity: update.quantity.or_else(|| Some(item.quantity)).unwrap(), + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let changes = UpdateItem { + product_id: pid, + quantity: update.quantity.map(|q| q as i32), price: new_price, - status: update.status.or_else(|| Some(item.status)).unwrap(), + status: update.status.map(|s| order_status_str(s).to_string()), }; - // Update the item in the database - let updated_item: Option = DB - .update((ITEMS, &id)) - .content(updated) - .await?; - - if let Some(updated) = updated_item { - let item_type: types::Item = updated.into(); - - // Broadcast the updated item - broadcast_update(item_type.clone()); - - Ok(item_type) - } else { - Err(ServerError("Failed to update item".into())) - } + + let item: DbItem = diesel::update(items.filter(dsl::id.eq(iid))) + .set(&changes) + .returning(DbItem::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update item: {}", e)))?; + + let item_type: types::Item = item.into(); + broadcast_update(item_type.clone()); + Ok(item_type) } #[server(DeleteItem, "/api/item")] -pub async fn delete_item(id: String) -> Result<(), ServerFnError> { +pub async fn delete_item(item_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; use crate::backend::websocket::broadcast_delete; - - let deleted: Option = DB.delete((ITEMS, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("Item with id {} not found", id))); + use crate::backend::schema::items::dsl::{self, items}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let iid: i32 = item_id.parse() + .map_err(|_| Error::InternalError("Invalid item ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(items.filter(dsl::id.eq(iid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Item {} not found", item_id)).into()); } - - // Broadcast the item deletion - broadcast_delete::(id); - + broadcast_delete::(item_id); Ok(()) } #[server(GetItemsByStation, "/api/item")] pub async fn get_items_by_station(station_id: String) -> Result, ServerFnError> { - use crate::backend::station::get_station; - use crate::backend::product::get_product; - - // Get the station to access its filtering criteria - let station = get_station(station_id).await?; - - // Query items that match the station's criteria - let query = "SELECT * FROM items WHERE status IN $input_statuses"; - let mut response = DB.query(query) - .bind(("input_statuses", station.input_statuses)) - .await?; - - let items: Vec = response.take(0)?; - - // Filter items by category (need to check product category) - let mut filtered_items = Vec::new(); - for item in items { - let product = get_product(item.product_id.clone()).await?; - if station.category_ids.contains(&product.category_id) { - filtered_items.push(item); + use crate::backend::db::get_pool; + use crate::backend::models::DbItem; + use crate::backend::schema::items::dsl::{self, items}; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let sid: i32 = station_id.parse() + .map_err(|_| Error::InternalError("Invalid station ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + // Load station's accepted input statuses + let input_status_strs: Vec = sis_dsl::station_input_statuses + .filter(sis_dsl::station_id.eq(sid)) + .select(sis_dsl::status) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + // Load category IDs handled by this station + let cat_ids: Vec = sc_dsl::station_categories + .filter(sc_dsl::station_id.eq(sid)) + .select(sc_dsl::category_id) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + // Load matching items (correct status), then filter by category + let candidate_items: Vec = items + .filter(dsl::status.eq_any(&input_status_strs)) + .select(DbItem::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + // Filter by product category using a join query + use crate::backend::schema::products::dsl as p_dsl; + let mut filtered = Vec::new(); + for item in candidate_items { + let product_cat: Option = p_dsl::products + .filter(p_dsl::id.eq(item.product_id)) + .select(p_dsl::category_id) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + if let Some(cat_id) = product_cat { + if cat_ids.contains(&cat_id) { + filtered.push(item); + } } } - - Ok(filtered_items.into_iter().map(Into::into).collect()) + + Ok(filtered.into_iter().map(Into::into).collect()) } #[server(UpdateItemsByOrder, "/api/item")] -pub async fn update_items_by_order(order_id: String, station_id: String, new_status: types::OrderStatus) -> Result, ServerFnError> { - use crate::backend::station::get_station; - use crate::backend::product::get_product; +pub async fn update_items_by_order( + order_id: String, + station_id: String, + new_status: types::OrderStatus, +) -> Result, ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::models::{DbItem, order_status_str}; use crate::backend::websocket::broadcast_update; - - // Get the station to access its filtering criteria - let station = get_station(station_id).await?; - - // Get all items for the order - let query = "SELECT * FROM items WHERE order_id = $order_id"; - let mut response = DB.query(query).bind(("order_id", order_id.clone())).await?; - let items: Vec = response.take(0)?; - + use crate::backend::schema::items::dsl::{self, items}; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let oid: i32 = order_id.parse() + .map_err(|_| Error::InternalError("Invalid order ID".to_string()))?; + let sid: i32 = station_id.parse() + .map_err(|_| Error::InternalError("Invalid station ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let input_status_strs: Vec = sis_dsl::station_input_statuses + .filter(sis_dsl::station_id.eq(sid)) + .select(sis_dsl::status) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let cat_ids: Vec = sc_dsl::station_categories + .filter(sc_dsl::station_id.eq(sid)) + .select(sc_dsl::category_id) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let order_items: Vec = items + .filter(dsl::order_id.eq(oid)) + .filter(dsl::status.eq_any(&input_status_strs)) + .select(DbItem::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + use crate::backend::schema::products::dsl as p_dsl; + let new_status_str = order_status_str(new_status).to_string(); let mut updated_items = Vec::new(); - - // Filter and update only items that belong to products in the station's categories AND match the station's input statuses - for item in items { - let product = get_product(item.product_id.clone()).await?; - - // Only update items whose products belong to this station's categories AND have the correct status - if station.category_ids.contains(&product.category_id) && station.input_statuses.contains(&item.status) { - let updated = Item { - id: item.id.clone(), - order_id: item.order_id, - product_id: item.product_id, - quantity: item.quantity, - price: item.price, - status: new_status, - }; - - let updated_item: Option = DB - .update((ITEMS, item.id.unwrap().key().to_string().as_str())) - .content(updated) - .await?; - - if let Some(item) = updated_item { - let item_type: types::Item = item.into(); - - // Broadcast each updated item + + for item in order_items { + let product_cat: Option = p_dsl::products + .filter(p_dsl::id.eq(item.product_id)) + .select(p_dsl::category_id) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + if let Some(cat_id) = product_cat { + if cat_ids.contains(&cat_id) { + let updated: DbItem = diesel::update(items.filter(dsl::id.eq(item.id))) + .set(dsl::status.eq(&new_status_str)) + .returning(DbItem::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let item_type: types::Item = updated.into(); broadcast_update(item_type.clone()); - updated_items.push(item_type); } } } - + Ok(updated_items) } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a9863d8..54bf76d 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -6,6 +6,10 @@ pub mod db; pub mod auth; #[cfg(feature = "ssr")] pub mod websocket; +#[cfg(feature = "ssr")] +pub mod schema; +#[cfg(feature = "ssr")] +pub mod models; pub mod category; pub mod event; diff --git a/src/backend/models.rs b/src/backend/models.rs new file mode 100644 index 0000000..3eb1f3f --- /dev/null +++ b/src/backend/models.rs @@ -0,0 +1,384 @@ +use chrono::{DateTime, Utc}; +use diesel::prelude::*; +use uuid::Uuid; + +use crate::backend::schema::*; +use crate::common::types; + +// --------------------------------------------------------------------------- +// Users +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = users)] +pub struct DbUser { + pub id: Uuid, + pub email: String, + pub password_hash: String, + pub role: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = users)] +pub struct NewUser<'a> { + pub email: &'a str, + pub password_hash: &'a str, + pub role: &'a str, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = users)] +pub struct UpdateUser { + pub email: Option, + pub role: Option, +} + +impl From for types::User { + fn from(u: DbUser) -> Self { + Self { + id: u.id.to_string(), + email: u.email, + role: u.role.parse_role(), + } + } +} + +// --------------------------------------------------------------------------- +// Categories +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = categories)] +pub struct DbCategory { + pub id: i32, + pub name: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = categories)] +pub struct NewCategory<'a> { + pub name: &'a str, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = categories)] +pub struct UpdateCategory { + pub name: Option, +} + +impl From for types::Category { + fn from(c: DbCategory) -> Self { + Self { + id: c.id.to_string(), + name: c.name, + } + } +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = events)] +pub struct DbEvent { + pub id: i32, + pub name: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = events)] +pub struct NewEvent<'a> { + pub name: &'a str, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = events)] +pub struct UpdateEvent { + pub name: Option, +} + +impl From for types::Event { + fn from(e: DbEvent) -> Self { + Self { + id: e.id.to_string(), + name: e.name, + } + } +} + +// --------------------------------------------------------------------------- +// Products +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = products)] +pub struct DbProduct { + pub id: i32, + pub name: String, + pub category_id: i32, + pub price: f64, + pub active: bool, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = products)] +pub struct NewProduct<'a> { + pub name: &'a str, + pub category_id: i32, + pub price: f64, + pub active: bool, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = products)] +pub struct UpdateProduct { + pub name: Option, + pub category_id: Option, + pub price: Option, + pub active: Option, +} + +impl From for types::Product { + fn from(p: DbProduct) -> Self { + Self { + id: p.id.to_string(), + name: p.name, + category_id: p.category_id.to_string(), + price: p.price, + active: p.active, + } + } +} + +// --------------------------------------------------------------------------- +// Orders +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = orders)] +pub struct DbOrder { + pub id: i32, + pub event_id: i32, + pub created_at: DateTime, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = orders)] +pub struct NewOrder { + pub event_id: i32, +} + +impl From for types::Order { + fn from(o: DbOrder) -> Self { + Self { + id: o.id.to_string(), + event_id: o.event_id.to_string(), + } + } +} + +// --------------------------------------------------------------------------- +// Items +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = items)] +pub struct DbItem { + pub id: i32, + pub order_id: i32, + pub product_id: i32, + pub quantity: i32, + pub price: f64, + pub status: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = items)] +pub struct NewItem { + pub order_id: i32, + pub product_id: i32, + pub quantity: i32, + pub price: f64, + pub status: String, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = items)] +pub struct UpdateItem { + pub product_id: Option, + pub quantity: Option, + pub price: Option, + pub status: Option, +} + +impl From for types::Item { + fn from(i: DbItem) -> Self { + Self { + id: i.id.to_string(), + order_id: i.order_id.to_string(), + product_id: i.product_id.to_string(), + quantity: i.quantity as u32, + price: i.price, + status: i.status.parse_order_status(), + } + } +} + +// --------------------------------------------------------------------------- +// Stations +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = stations)] +pub struct DbStation { + pub id: i32, + pub name: String, + pub output_status: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = stations)] +pub struct NewStation<'a> { + pub name: &'a str, + pub output_status: &'a str, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = stations)] +pub struct UpdateStation { + pub name: Option, + pub output_status: Option, +} + +#[derive(Debug, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = station_categories)] +pub struct StationCategory { + pub station_id: i32, + pub category_id: i32, +} + +#[derive(Debug, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = station_input_statuses)] +pub struct StationInputStatus { + pub station_id: i32, + pub status: String, +} + +/// Assemble a full `types::Station` from a DB row + junction rows. +pub fn station_from_parts( + s: DbStation, + category_ids: Vec, + input_statuses: Vec, +) -> types::Station { + types::Station { + id: s.id.to_string(), + name: s.name, + category_ids: category_ids.into_iter().map(|id| id.to_string()).collect(), + input_statuses: input_statuses + .into_iter() + .map(|st| st.parse_order_status()) + .collect(), + output_status: s.output_status.parse_order_status(), + } +} + +// --------------------------------------------------------------------------- +// Settings +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable)] +#[diesel(table_name = settings)] +pub struct DbSettings { + pub id: i32, + pub active_event_id: Option, +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = settings)] +pub struct UpdateSettings { + pub active_event_id: Option>, +} + +impl From for types::Settings { + fn from(s: DbSettings) -> Self { + Self { + id: s.id.to_string(), + active_event_id: s.active_event_id.map(|id| id.to_string()), + } + } +} + +// --------------------------------------------------------------------------- +// Sessions +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = sessions)] +pub struct DbSession { + pub id: String, + pub user_id: Uuid, + pub expiry_date: DateTime, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = sessions)] +pub struct NewSession { + pub id: String, + pub user_id: Uuid, + pub expiry_date: DateTime, +} + +// --------------------------------------------------------------------------- +// Enum parsing helpers (TEXT <-> Rust enums) +// --------------------------------------------------------------------------- + +pub trait ParseRole { + fn parse_role(&self) -> types::Role; +} + +impl ParseRole for String { + fn parse_role(&self) -> types::Role { + match self.as_str() { + "Admin" => types::Role::Admin, + "Cashier" => types::Role::Cashier, + "Staff" => types::Role::Staff, + _ => types::Role::Visitor, + } + } +} + +pub trait ParseOrderStatus { + fn parse_order_status(&self) -> types::OrderStatus; +} + +impl ParseOrderStatus for String { + fn parse_order_status(&self) -> types::OrderStatus { + match self.as_str() { + "Draft" => types::OrderStatus::Draft, + "Ready" => types::OrderStatus::Ready, + "Completed" => types::OrderStatus::Completed, + "Cancelled" => types::OrderStatus::Cancelled, + _ => types::OrderStatus::Ordered, + } + } +} + +pub fn order_status_str(s: types::OrderStatus) -> &'static str { + match s { + types::OrderStatus::Draft => "Draft", + types::OrderStatus::Ordered => "Ordered", + types::OrderStatus::Ready => "Ready", + types::OrderStatus::Completed => "Completed", + types::OrderStatus::Cancelled => "Cancelled", + } +} + +pub fn role_str(r: &types::Role) -> &'static str { + match r { + types::Role::Admin => "Admin", + types::Role::Cashier => "Cashier", + types::Role::Staff => "Staff", + types::Role::Visitor => "Visitor", + } +} diff --git a/src/backend/order.rs b/src/backend/order.rs index 9059c44..324374f 100644 --- a/src/backend/order.rs +++ b/src/backend/order.rs @@ -3,95 +3,109 @@ use leptos::prelude::*; use crate::common::{requests, types}; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::{Datetime, Thing}; - use surrealdb::RecordId; - pub use validator::Validate; - - pub const ORDERS: &str = "orders"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Order { - pub id: Option, - #[validate(length(min = 1))] - pub event: String, - pub created_at: Datetime, - } - - impl From for types::Order { - fn from(record: Order) -> Self { - Self { - id: record.id.unwrap().key().to_string(), - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; +use crate::common::errors::Error; #[server(CreateOrder, "/api/order")] pub async fn create_order(req: requests::order::Create) -> Result { - use crate::backend::item::ssr::create_items; + use crate::backend::db::get_pool; + use crate::backend::models::{DbOrder, NewOrder}; use crate::backend::websocket::broadcast_add; - - // First create the order - let order: Option = DB.create(ORDERS) - .content(Order { - id: None, - event: req.event, - created_at: Datetime::default(), - }) - .await?; - - if order.is_none() { - return Err(ServerError("Failed to create order".to_string().into())); - } - let order = order.unwrap(); - let order_id = order.id.as_ref().unwrap().key().to_string(); - - // Convert to types::Order for broadcasting - let order_type: types::Order = order.clone().into(); - - // Broadcast the new order + use crate::backend::schema::orders::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let eid: i32 = req.event.parse() + .map_err(|_| Error::InternalError("Invalid event ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let order: DbOrder = diesel::insert_into(orders) + .values(NewOrder { event_id: eid }) + .returning(DbOrder::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create order: {}", e)))?; + + let order_type: types::Order = order.into(); broadcast_add(order_type.clone()); - - // Then create all the items + if !req.items.is_empty() { - create_items(order_id, req.items).await?; + use crate::backend::item::create_items; + create_items(order_type.id.clone(), req.items).await?; } - + Ok(order_type) } #[server(GetOrders, "/api/order")] pub async fn get_orders() -> Result, ServerFnError> { - let orders: Vec = DB.select(ORDERS).await?; - Ok(orders.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbOrder; + use crate::backend::schema::orders::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = orders + .select(DbOrder::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetOrder, "/api/order")] -pub async fn get_order(id: String) -> Result { - let order: Option = DB.select((ORDERS, &id)).await?; - order - .map(Into::into) - .ok_or_else(|| ServerError("Order not found".into())) +pub async fn get_order(order_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbOrder; + use crate::backend::schema::orders::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let oid: i32 = order_id.parse() + .map_err(|_| Error::InternalError("Invalid order ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let order: Option = orders + .filter(id.eq(oid)) + .select(DbOrder::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + order.map(Into::into) + .ok_or_else(|| Error::InternalError("Order not found".to_string()).into()) } #[server(DeleteOrder, "/api/order")] -pub async fn delete_order(id: String) -> Result<(), ServerFnError> { +pub async fn delete_order(order_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; use crate::backend::websocket::broadcast_delete; - - let deleted: Option = DB.delete((ORDERS, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("Order with id {} not found", id))); + use crate::backend::schema::orders::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let oid: i32 = order_id.parse() + .map_err(|_| Error::InternalError("Invalid order ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(orders.filter(id.eq(oid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Order {} not found", order_id)).into()); } - - // Broadcast the order deletion - broadcast_delete::(id); - + broadcast_delete::(order_id); Ok(()) } diff --git a/src/backend/product.rs b/src/backend/product.rs index 493ae21..0cc262e 100644 --- a/src/backend/product.rs +++ b/src/backend/product.rs @@ -3,119 +3,146 @@ use crate::common::{requests, types}; use leptos::prelude::*; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::backend::websocket::{broadcast_add, broadcast_delete, broadcast_update}; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; - pub const PRODUCTS: &str = "products"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Product { - pub id: Option, - #[validate(length(min = 1, max = 100))] - pub name: String, - pub category_id: String, - #[validate(range(min = 0.0))] - pub price: f64, - pub active: bool, - } - - impl From for types::Product { - fn from(record: Product) -> Self { - Self { - id: record.id.unwrap().key().to_string(), - name: record.name, - category_id: record.category_id, - price: record.price, - active: record.active, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; +use crate::common::errors::Error; #[server(CreateProduct, "/api/product")] pub async fn create_product( req: requests::product::Create, ) -> Result { - let p: Option = DB - .create(PRODUCTS) - .content(Product { - id: None, - name: req.name, - category_id: req.category_id, - price: req.price, - active: true, - }) - .await?; - if let Some(product) = p { - let result: types::Product = product.clone().into(); - broadcast_add(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to create product".into())) - } + use crate::backend::db::get_pool; + use crate::backend::models::{DbProduct, NewProduct}; + use crate::backend::websocket::broadcast_add; + use crate::backend::schema::products::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let cid: i32 = req.category_id.parse() + .map_err(|_| Error::InternalError("Invalid category ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let product: DbProduct = diesel::insert_into(products) + .values(NewProduct { name: &req.name, category_id: cid, price: req.price, active: true }) + .returning(DbProduct::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create product: {}", e)))?; + + let result: types::Product = product.into(); + broadcast_add(result.clone()); + Ok(result) } #[server(GetProducts, "/api/product")] pub async fn get_products() -> Result, ServerFnError> { - let products: Vec = DB.select(PRODUCTS).await?; - Ok(products.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbProduct; + use crate::backend::schema::products::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = products + .select(DbProduct::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetProduct, "/api/product")] -pub async fn get_product(id: String) -> Result { - let product: Option = DB.select((PRODUCTS, &id)).await?; - product - .map(Into::into) - .ok_or_else(|| ServerError("Product not found".into())) +pub async fn get_product(product_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbProduct; + use crate::backend::schema::products::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pid: i32 = product_id.parse() + .map_err(|_| Error::InternalError("Invalid product ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let product: Option = products + .filter(id.eq(pid)) + .select(DbProduct::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + product.map(Into::into) + .ok_or_else(|| Error::InternalError("Product not found".to_string()).into()) } #[server(UpdateProduct, "/api/product")] pub async fn update_product( - id: String, + product_id: String, update: requests::product::Update, ) -> Result { - // Get the existing product - let existing_product: Option = DB.select((PRODUCTS, &id)).await?; - if existing_product.is_none() { - return Err(ServerError("Product not found".into())); - } - let product = existing_product.unwrap(); - let updated = Product { - id: product.id, - name: update.name.or_else(|| Some(product.name)).unwrap(), - category_id: update.category_id.or_else(|| Some(product.category_id)).unwrap(), - price: update.price.or_else(|| Some(product.price)).unwrap(), - active: update.active.or_else(|| Some(product.active)).unwrap(), + use crate::backend::db::get_pool; + use crate::backend::models::{DbProduct, UpdateProduct}; + use crate::backend::websocket::broadcast_update; + use crate::backend::schema::products::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pid: i32 = product_id.parse() + .map_err(|_| Error::InternalError("Invalid product ID".to_string()))?; + + let cid: Option = update.category_id + .map(|s| s.parse().map_err(|_| Error::InternalError("Invalid category ID".to_string()))) + .transpose()?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let changes = UpdateProduct { + name: update.name, + category_id: cid, + price: update.price, + active: update.active, }; - // Update the product in the database - let updated_product: Option = DB - .update((PRODUCTS, &id)) - .content(updated) - .await?; - - if let Some(product) = updated_product { - let result: types::Product = product.into(); - broadcast_update(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to update product".into())) - } + + let product: DbProduct = diesel::update(products.filter(id.eq(pid))) + .set(&changes) + .returning(DbProduct::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update product: {}", e)))?; + + let result: types::Product = product.into(); + broadcast_update(result.clone()); + Ok(result) } #[server(DeleteProduct, "/api/product")] -pub async fn delete_product(id: String) -> Result<(), ServerFnError> { - let deleted: Option = DB.delete((PRODUCTS, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("Product with id {} not found", id))); +pub async fn delete_product(product_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::websocket::broadcast_delete; + use crate::backend::schema::products::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pid: i32 = product_id.parse() + .map_err(|_| Error::InternalError("Invalid product ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(products.filter(id.eq(pid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Product {} not found", product_id)).into()); } - broadcast_delete::(id); + broadcast_delete::(product_id); Ok(()) } diff --git a/src/backend/schema.rs b/src/backend/schema.rs new file mode 100644 index 0000000..73a01f8 --- /dev/null +++ b/src/backend/schema.rs @@ -0,0 +1,119 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + categories (id) { + id -> Int4, + #[max_length = 64] + name -> Varchar, + } +} + +diesel::table! { + events (id) { + id -> Int4, + #[max_length = 64] + name -> Varchar, + } +} + +diesel::table! { + items (id) { + id -> Int4, + order_id -> Int4, + product_id -> Int4, + quantity -> Int4, + price -> Float8, + status -> Text, + } +} + +diesel::table! { + orders (id) { + id -> Int4, + event_id -> Int4, + created_at -> Timestamptz, + } +} + +diesel::table! { + products (id) { + id -> Int4, + #[max_length = 100] + name -> Varchar, + category_id -> Int4, + price -> Float8, + active -> Bool, + } +} + +diesel::table! { + sessions (id) { + id -> Text, + user_id -> Uuid, + expiry_date -> Timestamptz, + } +} + +diesel::table! { + settings (id) { + id -> Int4, + active_event_id -> Nullable, + } +} + +diesel::table! { + station_categories (station_id, category_id) { + station_id -> Int4, + category_id -> Int4, + } +} + +diesel::table! { + station_input_statuses (station_id, status) { + station_id -> Int4, + status -> Text, + } +} + +diesel::table! { + stations (id) { + id -> Int4, + #[max_length = 64] + name -> Varchar, + output_status -> Text, + } +} + +diesel::table! { + users (id) { + id -> Uuid, + #[max_length = 255] + email -> Varchar, + password_hash -> Text, + role -> Text, + } +} + +diesel::joinable!(items -> orders (order_id)); +diesel::joinable!(items -> products (product_id)); +diesel::joinable!(orders -> events (event_id)); +diesel::joinable!(products -> categories (category_id)); +diesel::joinable!(sessions -> users (user_id)); +diesel::joinable!(settings -> events (active_event_id)); +diesel::joinable!(station_categories -> categories (category_id)); +diesel::joinable!(station_categories -> stations (station_id)); +diesel::joinable!(station_input_statuses -> stations (station_id)); + +diesel::allow_tables_to_appear_in_same_query!( + categories, + events, + items, + orders, + products, + sessions, + settings, + station_categories, + station_input_statuses, + stations, + users, +); diff --git a/src/backend/settings.rs b/src/backend/settings.rs index 24899ba..0000f05 100644 --- a/src/backend/settings.rs +++ b/src/backend/settings.rs @@ -3,93 +3,73 @@ use leptos::prelude::*; use crate::common::{requests, types}; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::backend::websocket::{broadcast_add, broadcast_delete, broadcast_update}; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; +use crate::common::errors::Error; - pub const SETTINGS: &str = "settings"; - pub const SETTINGS_ID: &str = "global"; +#[server(GetSettings, "/api/settings")] +pub async fn get_settings() -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbSettings; + use crate::backend::schema::settings::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Settings { - pub id: Option, - pub active_event_id: Option, - } + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; - impl From for types::Settings { - fn from(settings: Settings) -> Self { - Self { - id: settings.id.unwrap().key().to_string(), - active_event_id: settings.active_event_id, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; + let s: DbSettings = settings + .select(DbSettings::as_select()) + .first(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to load settings: {}", e)))?; -#[server(GetSettings, "/api/settings")] -pub async fn get_settings() -> Result { - // Try to get existing settings - let existing: Option = DB.select((SETTINGS, SETTINGS_ID)).await?; - - if let Some(settings) = existing { - Ok(settings.into()) - } else { - // Create default settings if they don't exist - let default_settings: Option = DB - .create((SETTINGS, SETTINGS_ID)) - .content(Settings { - id: None, - active_event_id: None, - }) - .await?; - - default_settings - .map(Into::into) - .ok_or_else(|| ServerError("Failed to create default settings".into())) - } + Ok(s.into()) } #[server(UpdateSettings, "/api/settings")] pub async fn update_settings( update: requests::settings::Update, ) -> Result { - // Get existing settings or create default - let current_settings = get_settings().await?; - - let updated = Settings { - id: None, // Will be ignored by SurrealDB for updates - active_event_id: update.active_event_id.or(current_settings.active_event_id), + use crate::backend::db::get_pool; + use crate::backend::models::{DbSettings, UpdateSettings}; + use crate::backend::websocket::broadcast_update; + use crate::backend::schema::settings::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + // Convert Option → Option> + // Some("123") → Some(Some(123)) + // Some("") or None → Some(None) clears the value + let new_event_id: Option> = match update.active_event_id { + Some(ref s) if !s.is_empty() => { + let eid: i32 = s.parse() + .map_err(|_| Error::InternalError("Invalid event ID".to_string()))?; + Some(Some(eid)) + } + Some(_) => Some(None), // empty string clears the event + None => None, // no change }; - - // Update the settings in the database - let updated_settings: Option = DB - .update((SETTINGS, SETTINGS_ID)) - .content(updated) - .await?; - - if let Some(settings) = updated_settings { - let result: types::Settings = settings.clone().into(); - broadcast_update(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to update settings".into())) - } + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let s: DbSettings = diesel::update(settings.filter(id.eq(1))) + .set(UpdateSettings { active_event_id: new_event_id }) + .returning(DbSettings::as_select()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update settings: {}", e)))?; + + let result: types::Settings = s.into(); + broadcast_update(result.clone()); + Ok(result) } #[server(SetActiveEvent, "/api/settings")] pub async fn set_active_event(event_id: String) -> Result { - let update = requests::settings::Update { + update_settings(requests::settings::Update { active_event_id: Some(event_id), - }; - update_settings(update).await + }) + .await } #[server(GetActiveEvent, "/api/settings")] diff --git a/src/backend/station.rs b/src/backend/station.rs index f210017..e530eec 100644 --- a/src/backend/station.rs +++ b/src/backend/station.rs @@ -3,41 +3,7 @@ use leptos::prelude::*; use crate::common::types; #[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::db::DB; - pub use crate::backend::websocket::{broadcast_add, broadcast_delete, broadcast_update}; - pub use crate::common::types; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::Thing; - use surrealdb::RecordId; - pub use validator::Validate; - pub const STATIONS: &str = "stations"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct Station { - pub id: Option, - #[validate(length(min = 1, max = 64))] - pub name: String, - pub category_ids: Vec, - pub input_statuses: Vec, - pub output_status: types::OrderStatus, - } - - impl From for types::Station { - fn from(station: Station) -> Self { - Self { - id: station.id.unwrap().key().to_string(), - name: station.name, - category_ids: station.category_ids, - input_statuses: station.input_statuses, - output_status: station.output_status, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; +use crate::common::errors::Error; #[server(CreateStation, "/api/station")] pub async fn create_station( @@ -46,127 +12,273 @@ pub async fn create_station( input_statuses_json: String, output_status: types::OrderStatus, ) -> Result { - // Deserialize the JSON arrays + use crate::backend::db::get_pool; + use crate::backend::models::{DbStation, NewStation, StationCategory, StationInputStatus, station_from_parts, order_status_str}; + use crate::backend::websocket::broadcast_add; + use crate::backend::schema::stations::dsl::stations; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + let category_ids: Vec = if category_ids_json.is_empty() { - Vec::new() + vec![] } else { - match serde_json::from_str(&category_ids_json) { - Ok(ids) => ids, - Err(_) => { - return Err(ServerError("Failed to parse category_ids".into())); - } - } + serde_json::from_str(&category_ids_json) + .map_err(|_| Error::InternalError("Failed to parse category_ids".to_string()))? }; - + let input_statuses: Vec = if input_statuses_json.is_empty() { - Vec::new() + vec![] } else { - match serde_json::from_str(&input_statuses_json) { - Ok(statuses) => statuses, - Err(_) => { - return Err(ServerError("Failed to parse input_statuses".into())); - } - } + serde_json::from_str(&input_statuses_json) + .map_err(|_| Error::InternalError("Failed to parse input_statuses".to_string()))? }; - - let s: Option = DB.create(STATIONS) - .content(Station { - id: None, - name: name.clone(), - category_ids, - input_statuses, - output_status, - }) - .await?; - if let Some(station) = s { - let result: types::Station = station.into(); - broadcast_add(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to create station".into())) + + let cat_ids_i32: Vec = category_ids.iter() + .map(|s| s.parse().map_err(|_| Error::InternalError("Invalid category ID".to_string()))) + .collect::>()?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let station: DbStation = diesel::insert_into(stations) + .values(NewStation { name: &name, output_status: order_status_str(output_status) }) + .returning(DbStation::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create station: {}", e)))?; + + // Insert junction rows + let sc_rows: Vec = cat_ids_i32.iter() + .map(|&cid| StationCategory { station_id: station.id, category_id: cid }) + .collect(); + if !sc_rows.is_empty() { + diesel::insert_into(sc_dsl::station_categories) + .values(&sc_rows) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + } + + let sis_rows: Vec = input_statuses.iter() + .map(|&s| StationInputStatus { station_id: station.id, status: order_status_str(s).to_string() }) + .collect(); + if !sis_rows.is_empty() { + diesel::insert_into(sis_dsl::station_input_statuses) + .values(&sis_rows) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; } + + let result = station_from_parts( + station, + cat_ids_i32, + input_statuses.iter().map(|&s| order_status_str(s).to_string()).collect(), + ); + broadcast_add(result.clone()); + Ok(result) } #[server(GetStations, "/api/station")] pub async fn get_stations() -> Result, ServerFnError> { - let stations: Vec = DB.select(STATIONS).await?; - Ok(stations.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::{DbStation, station_from_parts}; + use crate::backend::schema::stations::dsl::stations; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let db_stations: Vec = stations + .select(DbStation::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let mut result = Vec::new(); + for station in db_stations { + let cat_ids: Vec = sc_dsl::station_categories + .filter(sc_dsl::station_id.eq(station.id)) + .select(sc_dsl::category_id) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let input_status_strs: Vec = sis_dsl::station_input_statuses + .filter(sis_dsl::station_id.eq(station.id)) + .select(sis_dsl::status) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + result.push(station_from_parts(station, cat_ids, input_status_strs)); + } + Ok(result) } #[server(GetStation, "/api/station")] -pub async fn get_station(name: String) -> Result { - let station: Option = DB.select((STATIONS, &name)).await?; - station - .map(Into::into) - .ok_or_else(|| ServerError("Station not found".into())) +pub async fn get_station(station_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::{DbStation, station_from_parts}; + use crate::backend::schema::stations::dsl::{stations, id}; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let sid: i32 = station_id.parse() + .map_err(|_| Error::InternalError("Invalid station ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let station: Option = stations + .filter(id.eq(sid)) + .select(DbStation::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + let station = station.ok_or_else(|| Error::InternalError("Station not found".to_string()))?; + + let cat_ids: Vec = sc_dsl::station_categories + .filter(sc_dsl::station_id.eq(sid)) + .select(sc_dsl::category_id) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let input_status_strs: Vec = sis_dsl::station_input_statuses + .filter(sis_dsl::station_id.eq(sid)) + .select(sis_dsl::status) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(station_from_parts(station, cat_ids, input_status_strs)) } #[server(UpdateStation, "/api/station")] pub async fn update_station( - id: String, + station_id: String, name: String, category_ids_json: String, input_statuses_json: String, output_status: types::OrderStatus, ) -> Result { - - // Deserialize the JSON arrays + use crate::backend::db::get_pool; + use crate::backend::models::{DbStation, UpdateStation, StationCategory, StationInputStatus, station_from_parts, order_status_str}; + use crate::backend::websocket::broadcast_update; + use crate::backend::schema::stations::dsl::{stations, id}; + use crate::backend::schema::station_categories::dsl as sc_dsl; + use crate::backend::schema::station_input_statuses::dsl as sis_dsl; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let sid: i32 = station_id.parse() + .map_err(|_| Error::InternalError("Invalid station ID".to_string()))?; + let category_ids: Vec = if category_ids_json.is_empty() { - Vec::new() + vec![] } else { - match serde_json::from_str(&category_ids_json) { - Ok(ids) => ids, - Err(_) => { - return Err(ServerError("Failed to parse category_ids".into())); - } - } + serde_json::from_str(&category_ids_json) + .map_err(|_| Error::InternalError("Failed to parse category_ids".to_string()))? }; - + let input_statuses: Vec = if input_statuses_json.is_empty() { - Vec::new() + vec![] } else { - match serde_json::from_str(&input_statuses_json) { - Ok(statuses) => statuses, - Err(_) => { - return Err(ServerError("Failed to parse input_statuses".into())); - } - } + serde_json::from_str(&input_statuses_json) + .map_err(|_| Error::InternalError("Failed to parse input_statuses".to_string()))? }; - - // Get the existing station - let existing_station: Option = DB.select((STATIONS, &id)).await?; - if existing_station.is_none() { - return Err(ServerError("Station not found".into())); + + let cat_ids_i32: Vec = category_ids.iter() + .map(|s| s.parse().map_err(|_| Error::InternalError("Invalid category ID".to_string()))) + .collect::>()?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let station: DbStation = diesel::update(stations.filter(id.eq(sid))) + .set(UpdateStation { + name: Some(name), + output_status: Some(order_status_str(output_status).to_string()), + }) + .returning(DbStation::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update station: {}", e)))?; + + // Replace junction rows + diesel::delete(sc_dsl::station_categories.filter(sc_dsl::station_id.eq(sid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + diesel::delete(sis_dsl::station_input_statuses.filter(sis_dsl::station_id.eq(sid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let sc_rows: Vec = cat_ids_i32.iter() + .map(|&cid| StationCategory { station_id: sid, category_id: cid }) + .collect(); + if !sc_rows.is_empty() { + diesel::insert_into(sc_dsl::station_categories) + .values(&sc_rows) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; } - let station = existing_station.unwrap(); - let updated = Station { - id: station.id, - name, // Allow name to be updated - category_ids, - input_statuses, - output_status, - }; - // Update the station in the database - let updated_station: Option = DB - .update((STATIONS, &id)) - .content(updated) - .await?; - - if let Some(station) = updated_station { - let result: types::Station = station.into(); - broadcast_update(result.clone()); - Ok(result) - } else { - Err(ServerError("Failed to update station".into())) + + let sis_rows: Vec = input_statuses.iter() + .map(|&s| StationInputStatus { station_id: sid, status: order_status_str(s).to_string() }) + .collect(); + if !sis_rows.is_empty() { + diesel::insert_into(sis_dsl::station_input_statuses) + .values(&sis_rows) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; } + + let result = station_from_parts( + station, + cat_ids_i32, + input_statuses.iter().map(|&s| order_status_str(s).to_string()).collect(), + ); + broadcast_update(result.clone()); + Ok(result) } #[server(DeleteStation, "/api/station")] -pub async fn delete_station(id: String) -> Result<(), ServerFnError> { - let deleted: Option = DB.delete((STATIONS, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("Station with id {} not found", id))); +pub async fn delete_station(station_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::websocket::broadcast_delete; + use crate::backend::schema::stations::dsl::{stations, id}; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let sid: i32 = station_id.parse() + .map_err(|_| Error::InternalError("Invalid station ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(stations.filter(id.eq(sid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("Station {} not found", station_id)).into()); } - broadcast_delete::(id); + broadcast_delete::(station_id); Ok(()) } diff --git a/src/backend/user.rs b/src/backend/user.rs index df1cab4..964141c 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -5,185 +5,216 @@ use crate::common::{requests, types}; #[cfg(feature = "ssr")] use crate::common::errors::Error; -#[cfg(feature = "ssr")] -pub mod ssr { - pub use crate::backend::{ - auth::{get_extended_expiry, should_extend_session, verify_password, SessionData}, - db::DB, - }; - pub use crate::common::types; - pub use leptos::logging::log; - pub use leptos::server_fn::error::ServerFnError::ServerError; - pub use leptos_axum::extract; - pub use serde::{Deserialize, Serialize}; - pub use surrealdb::sql::{Datetime, Thing}; - use surrealdb::RecordId; - pub use tower_sessions::Session; - pub use validator::Validate; - pub const USERS: &str = "users"; - - #[derive(Debug, Clone, Serialize, Deserialize, Validate)] - pub struct User { - pub id: Option, - #[validate(email)] - pub email: String, - pub password_hash: String, - pub role: types::Role, - } - - impl From for types::User { - fn from(user: User) -> Self { - types::User { - id: user.id.unwrap().key().to_string(), - email: user.email, - role: user.role, - } - } - } -} -#[cfg(feature = "ssr")] -use ssr::*; - #[server(CreateUser, "/api/user")] pub async fn create_user(req: requests::user::Create) -> Result { - use crate::backend::auth::hash_password; + use crate::backend::auth::{hash_password, SessionData, get_extended_expiry}; + use crate::backend::db::get_pool; + use crate::backend::models::{NewUser, DbUser, role_str}; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use tower_sessions::Session; + + let pw_hash = hash_password(&req.password) + .map_err(|e| Error::InternalError(e.to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let count: i64 = users + .count() + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + let assigned_role = if count == 0 { types::Role::Admin } else { types::Role::Visitor }; + + let new_user = NewUser { + email: &req.email, + password_hash: &pw_hash, + role: role_str(&assigned_role), + }; - let password_hash = hash_password(&req.password); - if password_hash.is_err() { - return Err(ServerError(format!("Password hashing failed."))); - } - let password_hash = password_hash.unwrap(); + let user: DbUser = diesel::insert_into(users) + .values(&new_user) + .returning(DbUser::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to create user: {}", e)))?; - // Check if this is the first user - let user_count = get_user_count().await; - if user_count.is_err() { - return Err(ServerError(format!("Database error!"))); - } - let user_count = user_count.unwrap(); - - let role = if user_count == 0 { - types::Role::Admin // First user becomes admin - } else { - types::Role::Visitor // All subsequent users become visitors - }; + // Auto-login: create session for the new user + let session: Session = leptos_axum::extract().await?; + let session_data = SessionData::new(user.id.to_string()); + session.insert("user", session_data).await + .map_err(|e| Error::InternalError(format!("Session error: {}", e)))?; + session.set_expiry(Some(tower_sessions::Expiry::AtDateTime(get_extended_expiry()))); - let u: Option = DB - .create(USERS) - .content(User { - id: None, - email: req.email, - password_hash, - role, - }) - .await?; - u.map(Into::into) - .ok_or_else(|| ServerError("Failed to create user".into())) + Ok(user.into()) } #[server(GetAllUsers, "/api/users")] pub async fn get_all_users() -> Result, ServerFnError> { - let users: Vec = DB.select(USERS).await?; - Ok(users.into_iter().map(Into::into).collect()) + use crate::backend::db::get_pool; + use crate::backend::models::DbUser; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let result: Vec = users + .select(DbUser::as_select()) + .load(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + Ok(result.into_iter().map(Into::into).collect()) } #[server(GetUser, "/api/user")] -pub async fn get_user(id: String) -> Result { - let user: Option = DB.select((USERS, id)).await?; - user - .map(Into::into) - .ok_or_else(|| ServerError("User not found".into())) -} - -#[cfg(feature = "ssr")] -pub async fn get_user_by_email(email: String) -> Result, surrealdb::Error> { - let mut result = DB - .query("SELECT * FROM users WHERE email = $email") - .bind(("email", email)) - .await?; - let user: Option = result.take(0)?; - Ok(user) +pub async fn get_user(user_id: String) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::DbUser; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + let uuid = Uuid::parse_str(&user_id) + .map_err(|_| Error::InternalError("Invalid user ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let user: Option = users + .filter(id.eq(uuid)) + .select(DbUser::as_select()) + .first(&mut conn) + .await + .optional() + .map_err(|e| Error::InternalError(e.to_string()))?; + + user.map(Into::into) + .ok_or_else(|| Error::InternalError("User not found".to_string()).into()) } #[cfg(feature = "ssr")] -pub async fn get_user_count() -> Result { - let users: Vec = DB.select(USERS).await?; - Ok(users.len() as u64) +pub async fn get_user_by_email(email_val: &str) -> Result, diesel::result::Error> { + use crate::backend::db::get_pool; + use crate::backend::models::DbUser; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + + let pool = get_pool(); + let mut conn = pool.get().await.expect("pool unavailable"); + + users + .filter(email.eq(email_val)) + .select(DbUser::as_select()) + .first(&mut conn) + .await + .optional() } #[server(UpdateUser, "/api/user")] pub async fn update_user( - id: String, + user_id: String, update: requests::user::Update, -) -> Result { - // Get the existing user - let existing_user: Option = DB.select((USERS, &id)).await?; - if existing_user.is_none() { - return Err(ServerError("User not found".into())); - } - let user = existing_user.unwrap(); - let updated = User { - id: user.id, - email: update.email.or_else(|| Some(user.email)).unwrap(), - password_hash: user.password_hash, - role: update.role.or_else(|| Some(user.role)).unwrap(), +) -> Result { + use crate::backend::db::get_pool; + use crate::backend::models::{DbUser, UpdateUser, role_str}; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + let uuid = Uuid::parse_str(&user_id) + .map_err(|_| Error::InternalError("Invalid user ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let changes = UpdateUser { + email: update.email, + role: update.role.as_ref().map(|r| role_str(r).to_string()), }; - // Update the user in the database - let updated_user: Option = DB - .update((USERS, &id)) - .content(updated) - .await?; - - updated_user - .map(Into::into) - .ok_or_else(|| ServerError("Failed to update user".into())) + + let user: DbUser = diesel::update(users.filter(id.eq(uuid))) + .set(&changes) + .returning(DbUser::as_returning()) + .get_result(&mut conn) + .await + .map_err(|e| Error::InternalError(format!("Failed to update user: {}", e)))?; + + Ok(user.into()) } #[server(DeleteUser, "/api/user")] -pub async fn delete_user(id: String) -> Result<(), ServerFnError> { - let deleted: Option = DB.delete((USERS, &id)).await?; - if deleted.is_none() { - return Err(ServerError(format!("User with id {} not found", id))); +pub async fn delete_user(user_id: String) -> Result<(), ServerFnError> { + use crate::backend::db::get_pool; + use crate::backend::schema::users::dsl::*; + use diesel::prelude::*; + use diesel_async::RunQueryDsl; + use uuid::Uuid; + + let uuid = Uuid::parse_str(&user_id) + .map_err(|_| Error::InternalError("Invalid user ID".to_string()))?; + + let pool = get_pool(); + let mut conn = pool.get().await.map_err(|e| Error::InternalError(e.to_string()))?; + + let rows = diesel::delete(users.filter(id.eq(uuid))) + .execute(&mut conn) + .await + .map_err(|e| Error::InternalError(e.to_string()))?; + + if rows == 0 { + return Err(Error::InternalError(format!("User {} not found", user_id)).into()); } Ok(()) } #[server(Login, "/api/user")] -pub async fn login(email: String, password: String) -> Result { - let session: Session = extract().await?; - // Find user by email - let user = get_user_by_email(email.clone()).await - .map_err(|e| Error::InternalError(format!("Database error: {}", e)))?; +pub async fn login(email_val: String, password: String) -> Result { + use crate::backend::auth::{verify_password, SessionData, get_extended_expiry}; + use crate::backend::models::ParseRole; + use tower_sessions::Session; + + let session: Session = leptos_axum::extract().await?; - let user = user.ok_or_else(|| Error::NotAuthorized("Invalid credentials".to_string()))?; + let user = get_user_by_email(&email_val) + .await + .map_err(|e| Error::InternalError(format!("Database error: {}", e)))? + .ok_or_else(|| Error::NotAuthorized("Invalid credentials".to_string()))?; - // Verify password if !verify_password(&password, &user.password_hash)? { return Err(Error::NotAuthorized("Invalid credentials".to_string()).into()); } - // Create session data - let session_data = SessionData::new(user.id.as_ref().unwrap().key().to_string()); - - // Store in session + let session_data = SessionData::new(user.id.to_string()); session.insert("user", session_data).await?; + session.set_expiry(Some(tower_sessions::Expiry::AtDateTime(get_extended_expiry()))); - // Set session expiry to 24 hours - session.set_expiry(Some(tower_sessions::Expiry::AtDateTime( - get_extended_expiry(), - ))); - - Ok(user.into()) + Ok(crate::common::types::User { + id: user.id.to_string(), + email: user.email, + role: user.role.parse_role(), + }) } #[server(Logout, "/api/user")] pub async fn logout() -> Result<(), ServerFnError> { - let session: Session = extract().await?; + use tower_sessions::Session; + let session: Session = leptos_axum::extract().await?; let _ = session.delete().await; Ok(()) } #[server(GetCurrentUser, "/api/user")] pub async fn get_current_user() -> Result { - let session: Session = extract().await?; + use tower_sessions::Session; + let session: Session = leptos_axum::extract().await?; crate::backend::auth::get_authenticated_user(&session).await } diff --git a/src/common/types.rs b/src/common/types.rs index cba10ee..8380a5a 100644 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -24,9 +24,10 @@ pub enum OrderStatus { pub struct Item { #[validate(length(min = 1))] pub id: String, - pub order_id: Option, // Reference to Order #[validate(length(min = 1))] - pub product_id: String, // Reference to Item + pub order_id: String, + #[validate(length(min = 1))] + pub product_id: String, // Reference to Product #[validate(range(min = 1))] pub quantity: u32, #[validate(range(min = 0.0))] @@ -68,6 +69,7 @@ pub struct Category { pub struct Order { #[validate(length(min = 1))] pub id: String, + pub event_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, Validate)] diff --git a/src/main.rs b/src/main.rs index 133ef2f..ff0093b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,10 @@ async fn main() { use order_stream::backend::db; use order_stream::backend::websocket::websocket_handler; use tower_sessions::{SessionManagerLayer, cookie::SameSite}; - use order_stream::backend::auth::SurrealSessionStore; + use order_stream::backend::auth::PostgresSessionStore; use tokio::sync::broadcast; - // Initialize database connection + // Initialize database connection pool if let Err(e) = db::initialize_database().await { eprintln!("Failed to initialize database: {}", e); std::process::exit(1); @@ -31,7 +31,7 @@ async fn main() { order_stream::backend::websocket::init_websocket_sender(ws_sender.clone()); // Configure sessions - let session_store = SurrealSessionStore::new(); + let session_store = PostgresSessionStore::new(); let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) .with_same_site(SameSite::Lax) @@ -54,7 +54,6 @@ async fn main() { .with_state(leptos_options); // run our app with hyper - // Server functions are automatically registered by Leptos let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); println!("listening on http://{}", &addr); println!("Server functions available at /api/*");