From ea135265155c81ed5e2f8a392a289ff5986b6bc6 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 13 Jul 2025 00:50:16 -0400 Subject: [PATCH] add: product types --- crates/app/src/public/css/style.css | 2 +- crates/app/src/public/html/mod/profile.lisp | 1 + crates/core/src/database/common.rs | 1 + crates/core/src/database/drivers/common.rs | 1 + .../database/drivers/sql/create_products.sql | 12 ++ crates/core/src/database/mod.rs | 1 + crates/core/src/database/products.rs | 138 ++++++++++++++++++ crates/core/src/model/permissions.rs | 1 + crates/core/src/model/products.rs | 14 +- 9 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 crates/core/src/database/drivers/sql/create_products.sql create mode 100644 crates/core/src/database/products.rs diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index c4c5185..53162c6 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -600,7 +600,7 @@ input[type="checkbox"]:checked { padding: 0; } -.notification .icon { +.notification:not(.chip) .icon { width: 100%; height: 100%; } diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 2121f1e..408b391 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -368,6 +368,7 @@ ADMINISTRATOR: 1 << 1, MANAGE_DOMAINS: 1 << 2, MANAGE_SERVICES: 1 << 3, + MANAGE_PRODUCTS: 1 << 4, }, \"secondary_role\", \"add_permission_to_secondary_role\", diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index f3d2668..e7cd0ef 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -42,6 +42,7 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap(); execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap(); execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap(); + execute!(&conn, common::CREATE_TABLE_PRODUCTS).unwrap(); self.0 .1 diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index 6a562e7..7bee30a 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -29,3 +29,4 @@ pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_mess pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql"); pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql"); pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql"); +pub const CREATE_TABLE_PRODUCTS: &str = include_str!("./sql/create_products.sql"); diff --git a/crates/core/src/database/drivers/sql/create_products.sql b/crates/core/src/database/drivers/sql/create_products.sql new file mode 100644 index 0000000..ff45afc --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_products.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS products ( + id BIGINT NOT NULL PRIMARY KEY, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + likes INT NOT NULL, + dislikes INT NOT NULL, + product_type TEXT NOT NULL, + stripe_id TEXT NOT NULL, + price TEXT NOT NULL +) diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 57873f9..730c54a 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -21,6 +21,7 @@ mod notifications; mod polls; mod pollvotes; mod posts; +mod products; mod questions; mod reactions; mod reports; diff --git a/crates/core/src/database/products.rs b/crates/core/src/database/products.rs new file mode 100644 index 0000000..10eb566 --- /dev/null +++ b/crates/core/src/database/products.rs @@ -0,0 +1,138 @@ +use crate::model::{ + auth::User, + products::Product, + permissions::{FinePermission, SecondaryPermission}, + Error, Result, +}; +use crate::{auto_method, DataManager}; +use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; + +impl DataManager { + /// Get a [`Product`] from an SQL row. + pub(crate) fn get_product_from_row(x: &PostgresRow) -> Product { + Product { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + name: get!(x->3(String)), + description: get!(x->4(String)), + likes: get!(x->5(i32)) as isize, + dislikes: get!(x->6(i32)) as isize, + product_type: serde_json::from_str(&get!(x->7(String))).unwrap(), + stripe_id: get!(x->8(String)), + price: serde_json::from_str(&get!(x->9(String))).unwrap(), + } + } + + auto_method!(get_product_by_id(usize as i64)@get_product_from_row -> "SELECT * FROM products WHERE id = $1" --name="product" --returns=Product --cache-key-tmpl="atto.product:{}"); + + /// Get all products by user. + /// + /// # Arguments + /// * `id` - the ID of the user to fetch products for + pub async fn get_products_by_user(&self, id: usize) -> Result> { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM products WHERE owner = $1 ORDER BY created DESC", + &[&(id as i64)], + |x| { Self::get_product_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("product".to_string())); + } + + Ok(res.unwrap()) + } + + const MAXIMUM_FREE_PRODUCTS: usize = 15; + + /// Create a new product in the database. + /// + /// # Arguments + /// * `data` - a mock [`Product`] object to insert + pub async fn create_product(&self, data: Product) -> Result { + // check values + if data.name.len() < 2 { + return Err(Error::DataTooShort("name".to_string())); + } else if data.name.len() > 128 { + return Err(Error::DataTooLong("name".to_string())); + } + + // check number of products + let owner = self.get_user_by_id(data.owner).await?; + + if !owner.permissions.check(FinePermission::SUPPORTER) { + let products = self.get_products_by_user(data.owner).await?; + + if products.len() >= Self::MAXIMUM_FREE_PRODUCTS { + return Err(Error::MiscError( + "You already have the maximum number of products you can have".to_string(), + )); + } + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.owner as i64), + &data.name, + &data.description, + &0_i32, + &0_i32, + &serde_json::to_string(&data.product_type).unwrap(), + &data.stripe_id, + &serde_json::to_string(&data.price).unwrap(), + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } + + pub async fn delete_product(&self, id: usize, user: &User) -> Result<()> { + let product = self.get_product_by_id(id).await?; + + // check user permission + if user.id != product.owner + && !user + .secondary_permissions + .check(SecondaryPermission::MANAGE_PRODUCTS) + { + return Err(Error::NotAllowed); + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, "DELETE FROM products WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // ... + self.0.1.remove(format!("atto.product:{}", id)).await; + Ok(()) + } +} diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 55cf9cc..bbaca18 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -176,6 +176,7 @@ bitflags! { const ADMINISTRATOR = 1 << 1; const MANAGE_DOMAINS = 1 << 2; const MANAGE_SERVICES = 1 << 3; + const MANAGE_PRODUCTS = 1 << 4; const _ = !0; } diff --git a/crates/core/src/model/products.rs b/crates/core/src/model/products.rs index e7b5b41..1b54ba3 100644 --- a/crates/core/src/model/products.rs +++ b/crates/core/src/model/products.rs @@ -6,11 +6,11 @@ pub struct Product { pub id: usize, pub created: usize, pub owner: usize, - pub title: String, + pub name: String, pub description: String, - pub likes: usize, - pub dislikes: usize, - pub r#type: ProductType, + pub likes: isize, + pub dislikes: isize, + pub product_type: ProductType, pub stripe_id: String, pub price: ProductPrice, } @@ -37,7 +37,7 @@ impl Product { /// Create a new [`Product`]. pub fn new( owner: usize, - title: String, + name: String, description: String, price: ProductPrice, r#type: ProductType, @@ -46,11 +46,11 @@ impl Product { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp(), owner, - title, + name, description, likes: 0, dislikes: 0, - r#type, + product_type: r#type, stripe_id: String::new(), price, }