remove: marketplace
This commit is contained in:
parent
2407e6b213
commit
b5f841a990
27 changed files with 22 additions and 1067 deletions
|
@ -55,7 +55,6 @@ pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.lisp");
|
|||
pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.lisp");
|
||||
pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.lisp");
|
||||
pub const AUTH_CONNECTION: &str = include_str!("./public/html/auth/connection.lisp");
|
||||
pub const AUTH_SELLER_CONNECTION: &str = include_str!("./public/html/auth/seller_connection.lisp");
|
||||
|
||||
pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.lisp");
|
||||
pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.lisp");
|
||||
|
@ -143,8 +142,6 @@ pub const LITTLEWEB_SERVICE: &str = include_str!("./public/html/littleweb/servic
|
|||
pub const LITTLEWEB_DOMAIN: &str = include_str!("./public/html/littleweb/domain.lisp");
|
||||
pub const LITTLEWEB_BROWSER: &str = include_str!("./public/html/littleweb/browser.lisp");
|
||||
|
||||
pub const MARKETPLACE_SELLER: &str = include_str!("./public/html/marketplace/seller.lisp");
|
||||
|
||||
pub const MAIL_RECEIVED: &str = include_str!("./public/html/mail/received.lisp");
|
||||
pub const MAIL_SENT: &str = include_str!("./public/html/mail/sent.lisp");
|
||||
pub const MAIL_COMPOSE: &str = include_str!("./public/html/mail/compose.lisp");
|
||||
|
@ -297,7 +294,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config --lisp plugins);
|
||||
write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config --lisp plugins);
|
||||
write_template!(html_path->"auth/connection.html"(crate::assets::AUTH_CONNECTION) --config=config --lisp plugins);
|
||||
write_template!(html_path->"auth/seller_connection.html"(crate::assets::AUTH_SELLER_CONNECTION) --config=config --lisp plugins);
|
||||
|
||||
write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config --lisp plugins);
|
||||
write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config --lisp plugins);
|
||||
|
@ -378,8 +374,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"littleweb/domain.html"(crate::assets::LITTLEWEB_DOMAIN) --config=config --lisp plugins);
|
||||
write_template!(html_path->"littleweb/browser.html"(crate::assets::LITTLEWEB_BROWSER) --config=config --lisp plugins);
|
||||
|
||||
write_template!(html_path->"marketplace/seller.html"(crate::assets::MARKETPLACE_SELLER) -d "marketplace" --config=config --lisp plugins);
|
||||
|
||||
write_template!(html_path->"mail/received.html"(crate::assets::MAIL_RECEIVED) -d "mail" --config=config --lisp plugins);
|
||||
write_template!(html_path->"mail/sent.html"(crate::assets::MAIL_SENT) --config=config --lisp plugins);
|
||||
write_template!(html_path->"mail/compose.html"(crate::assets::MAIL_COMPOSE) --config=config --lisp plugins);
|
||||
|
|
|
@ -316,12 +316,6 @@ version = "1.0.0"
|
|||
"littleweb:action.rename" = "Rename"
|
||||
"littleweb:action.add" = "Add"
|
||||
|
||||
"marketplace:label.products" = "Products"
|
||||
"marketplace:label.status" = "Status"
|
||||
"marketplace:action.get_started" = "Get started"
|
||||
"marketplace:action.finsh_setting_up_account" = "Finish setting up my account"
|
||||
"marketplace:action.open_seller_dashboard" = "Open seller dashboard"
|
||||
|
||||
"mail:label.received" = "Received"
|
||||
"mail:label.sent" = "Sent"
|
||||
"mail:label.compose" = "Compose"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
("class" "flex flex_col gap_2")
|
||||
("style" "max-width: 48ch")
|
||||
(h2
|
||||
("class" "w_full text-center")
|
||||
("class" "w_full text_center")
|
||||
; block for title
|
||||
(text "{% block title %}{% endblock %}"))
|
||||
(div
|
||||
|
|
|
@ -113,10 +113,9 @@
|
|||
|
||||
(text "{% endblock %} {% block footer %}")
|
||||
(span
|
||||
("class" "small w_full text-center")
|
||||
("class" "small w_full text_center")
|
||||
(text "Or, ")
|
||||
(a
|
||||
("href" "/auth/register")
|
||||
(text "register")))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -170,10 +170,9 @@
|
|||
|
||||
(text "{% endblock %} {% block footer %}")
|
||||
(span
|
||||
("class" "small w_full text-center")
|
||||
("class" "small w_full text_center")
|
||||
(text "Or, ")
|
||||
(a
|
||||
("href" "/auth/login")
|
||||
(text "login")))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
(text "{% extends \"auth/base.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Connection"))
|
||||
|
||||
(text "{% endblock %} {% block title %}Connection{% endblock %} {% block content %}")
|
||||
(div
|
||||
("class" "w_full flex_col gap_2")
|
||||
("id" "status")
|
||||
(b
|
||||
(text "Working...")))
|
||||
|
||||
(text "{% if connection_type == \"refresh\" %}")
|
||||
(script
|
||||
("defer" "true")
|
||||
(text "setTimeout(async () => {
|
||||
trigger(\"seller::onboarding\");
|
||||
}, 1000);"))
|
||||
(text "{% elif connection_type == \"return\" %}")
|
||||
(script
|
||||
("defer" "true")
|
||||
(text "setTimeout(async () => {
|
||||
document.getElementById(\"status\").innerHTML =
|
||||
`<b>Account updated.</b> You can now close this tab.`;
|
||||
}, 1000);"))
|
||||
(text "{%- endif %} {% endblock %}")
|
|
@ -2668,7 +2668,7 @@
|
|||
(td
|
||||
(a
|
||||
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
|
||||
("class" "flex gap_1 items_center")
|
||||
("class" "flex gap_1 items_center w_content")
|
||||
(text "{{ self::community_avatar(id=post.community, community=community) }}")
|
||||
(span
|
||||
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))))
|
||||
|
|
|
@ -376,17 +376,3 @@
|
|||
(span
|
||||
(text "{{ text \"settings:tab.connections\" }}")))
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro seller_settings_nav_options() -%}")
|
||||
(a
|
||||
("data-tab-button" "account")
|
||||
("class" "active")
|
||||
("href" "#/account")
|
||||
(icon (text "smile"))
|
||||
(span (str (text "settings:tab.account"))))
|
||||
(a
|
||||
("data-tab-button" "products")
|
||||
("href" "#/products")
|
||||
(icon (text "package"))
|
||||
(span (str (text "marketplace:label.products"))))
|
||||
(text "{%- endmacro %}")
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Seller settings - {{ config.name }}"))
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex_col gap_2")
|
||||
|
||||
; nav
|
||||
(div
|
||||
("class" "mobile_nav mobile")
|
||||
; primary nav
|
||||
(div
|
||||
("class" "dropdown")
|
||||
("style" "width: max-content")
|
||||
(button
|
||||
("class" "raised small")
|
||||
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
||||
("exclude" "dropdown")
|
||||
(icon (text "sliders-horizontal"))
|
||||
(span ("class" "current_tab_text") (text "account")))
|
||||
(div
|
||||
("class" "inner left")
|
||||
(text "{{ macros::seller_settings_nav_options() }}"))))
|
||||
|
||||
; nav desktop
|
||||
(div
|
||||
("class" "desktop pillmenu")
|
||||
(text "{{ macros::seller_settings_nav_options() }}"))
|
||||
|
||||
; ...
|
||||
(div
|
||||
("class" "card w_full lowered flex flex_col gap_2")
|
||||
("data-tab" "account")
|
||||
(div
|
||||
("class" "card_nest w_full")
|
||||
(div
|
||||
("class" "card small flex items_center gap_2")
|
||||
(div
|
||||
("class" "notification")
|
||||
("style" "width: 46px")
|
||||
(icon (text "stripe")))
|
||||
|
||||
(b (str (text "marketplace:label.status"))))
|
||||
|
||||
(div
|
||||
("class" "card")
|
||||
(text "{% if user.seller_data.account_id -%}")
|
||||
(text "{% if user.seller_data.completed_onboarding -%}")
|
||||
; completed onboarding + has stripe account linked
|
||||
(button
|
||||
("onclick" "trigger('seller::login')")
|
||||
(icon (text "arrow-right"))
|
||||
(str (text "marketplace:action.open_seller_dashboard")))
|
||||
(text "{% else %}")
|
||||
; not completed onboarding
|
||||
(p (text "You've not finished setting up your Stripe account."))
|
||||
(p (text "Please complete onboarding to accept payments."))
|
||||
|
||||
(button
|
||||
("onclick" "trigger('seller::onboarding')")
|
||||
(icon (text "arrow-right"))
|
||||
(str (text "marketplace:action.finsh_setting_up_account")))
|
||||
(text "{%- endif %}")
|
||||
(text "{% else %}")
|
||||
; doesn't have a stripe account linked
|
||||
(button
|
||||
("onclick" "trigger('seller::register')")
|
||||
(icon (text "arrow-right"))
|
||||
(str (text "marketplace:action.get_started")))
|
||||
(text "{%- endif %}"))))
|
||||
|
||||
(div
|
||||
("class" "card w_full lowered hidden flex flex_col gap_2")
|
||||
("data-tab" "products")
|
||||
(div
|
||||
("class" "card w_full flex flex_wrap gap_2")
|
||||
)))
|
||||
|
||||
(text "{% endblock %}")
|
|
@ -1205,60 +1205,3 @@
|
|||
]);
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const self = reg_ns("seller");
|
||||
|
||||
self.define("register", async () => {
|
||||
await trigger("atto::debounce", ["seller::register"]);
|
||||
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you want to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await (
|
||||
await fetch("/api/v1/service_hooks/stripe/seller/register", {
|
||||
method: "POST",
|
||||
})
|
||||
).json();
|
||||
|
||||
trigger("atto::toast", [res.ok ? "success" : "error", res.message]);
|
||||
self.onboarding();
|
||||
});
|
||||
|
||||
self.define("onboarding", async () => {
|
||||
await trigger("atto::debounce", ["seller::onboarding"]);
|
||||
|
||||
const res = await (
|
||||
await fetch("/api/v1/service_hooks/stripe/seller/onboarding", {
|
||||
method: "POST",
|
||||
})
|
||||
).json();
|
||||
|
||||
trigger("atto::toast", [res.ok ? "success" : "error", res.message]);
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = res.payload;
|
||||
}
|
||||
});
|
||||
|
||||
self.define("login", async () => {
|
||||
await trigger("atto::debounce", ["seller::login"]);
|
||||
|
||||
const res = await (
|
||||
await fetch("/api/v1/service_hooks/stripe/seller/login", {
|
||||
method: "POST",
|
||||
})
|
||||
).json();
|
||||
|
||||
trigger("atto::toast", [res.ok ? "success" : "error", res.message]);
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = res.payload;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use std::time::Duration;
|
||||
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
||||
use crate::cookie::CookieJar;
|
||||
use tetratto_core::model::{
|
||||
auth::{Notification, User},
|
||||
moderation::AuditLogEntry,
|
||||
|
@ -9,7 +7,7 @@ use tetratto_core::model::{
|
|||
ApiReturn, Error,
|
||||
};
|
||||
use stripe::{EventObject, EventType};
|
||||
use crate::{get_user_from_token, State};
|
||||
use crate::State;
|
||||
|
||||
pub async fn stripe_webhook(
|
||||
Extension(data): Extension<State>,
|
||||
|
@ -471,145 +469,3 @@ pub async fn stripe_webhook(
|
|||
payload: (),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn onboarding_account_link_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await);
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if user.seller_data.account_id.is_some() {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
let client = match data.3 {
|
||||
Some(ref c) => c,
|
||||
None => return Json(Error::Unknown.into()),
|
||||
};
|
||||
|
||||
match stripe::AccountLink::create(
|
||||
&client,
|
||||
stripe::CreateAccountLink {
|
||||
account: match user.seller_data.account_id {
|
||||
Some(id) => stripe::AccountId::from_str(&id).unwrap(),
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
},
|
||||
type_: stripe::AccountLinkType::AccountOnboarding,
|
||||
collect: None,
|
||||
expand: &[],
|
||||
refresh_url: Some(&format!(
|
||||
"{}/auth/connections_link/seller/refresh",
|
||||
data.0.0.0.host
|
||||
)),
|
||||
return_url: Some(&format!(
|
||||
"{}/auth/connections_link/seller/return",
|
||||
data.0.0.0.host
|
||||
)),
|
||||
collection_options: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(x) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Acceptable".to_string(),
|
||||
payload: Some(x.url),
|
||||
}),
|
||||
Err(e) => Json(Error::MiscError(e.to_string()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_seller_account_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await);
|
||||
let mut user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if user.seller_data.account_id.is_some() {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
let client = match data.3 {
|
||||
Some(ref c) => c,
|
||||
None => return Json(Error::Unknown.into()),
|
||||
};
|
||||
|
||||
let account = match stripe::Account::create(
|
||||
&client,
|
||||
stripe::CreateAccount {
|
||||
type_: Some(stripe::AccountType::Express),
|
||||
capabilities: Some(stripe::CreateAccountCapabilities {
|
||||
card_payments: Some(stripe::CreateAccountCapabilitiesCardPayments {
|
||||
requested: Some(true),
|
||||
}),
|
||||
transfers: Some(stripe::CreateAccountCapabilitiesTransfers {
|
||||
requested: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
user.seller_data.account_id = Some(account.id.to_string());
|
||||
match data
|
||||
.0
|
||||
.update_user_seller_data(user.id, user.seller_data)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Acceptable".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => return Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login_link_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await);
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if user.seller_data.account_id.is_none() | !user.seller_data.completed_onboarding {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
let client = match data.3 {
|
||||
Some(ref c) => c,
|
||||
None => return Json(Error::Unknown.into()),
|
||||
};
|
||||
|
||||
match stripe::LoginLink::create(
|
||||
&client,
|
||||
&stripe::AccountId::from_str(&user.seller_data.account_id.unwrap()).unwrap(),
|
||||
&data.0.0.0.host,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(x) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Acceptable".to_string(),
|
||||
payload: Some(x.url),
|
||||
}),
|
||||
Err(e) => Json(Error::MiscError(e.to_string()).into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{str::FromStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
use crate::{
|
||||
get_user_from_token,
|
||||
model::{ApiReturn, Error},
|
||||
|
@ -572,28 +572,11 @@ pub async fn delete_user_request(
|
|||
.delete_user(id, &req.password, user.permissions.check_manager())
|
||||
.await
|
||||
{
|
||||
Ok(ua) => {
|
||||
// delete stripe user
|
||||
if let Some(stripe_id) = ua.seller_data.account_id
|
||||
&& let Some(ref client) = data.3
|
||||
{
|
||||
if let Err(e) = stripe::Account::delete(
|
||||
&client,
|
||||
&stripe::AccountId::from_str(&stripe_id).unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Json(Error::MiscError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "User deleted".to_string(),
|
||||
payload: (),
|
||||
})
|
||||
}
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "User deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ pub mod journals;
|
|||
pub mod letters;
|
||||
pub mod notes;
|
||||
pub mod notifications;
|
||||
pub mod products;
|
||||
pub mod reactions;
|
||||
pub mod reports;
|
||||
pub mod requests;
|
||||
|
@ -34,7 +33,6 @@ use tetratto_core::model::{
|
|||
littleweb::{DomainData, DomainTld, ServiceFsEntry},
|
||||
oauth::AppScope,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
products::{ProductPrice, ProductType},
|
||||
reactions::AssetType,
|
||||
stacks::{StackMode, StackPrivacy, StackSort},
|
||||
};
|
||||
|
@ -566,18 +564,6 @@ pub fn routes() -> Router {
|
|||
"/service_hooks/stripe",
|
||||
post(auth::connections::stripe::stripe_webhook),
|
||||
)
|
||||
.route(
|
||||
"/service_hooks/stripe/seller/register",
|
||||
post(auth::connections::stripe::create_seller_account_request),
|
||||
)
|
||||
.route(
|
||||
"/service_hooks/stripe/seller/onboarding",
|
||||
post(auth::connections::stripe::onboarding_account_link_request),
|
||||
)
|
||||
.route(
|
||||
"/service_hooks/stripe/seller/login",
|
||||
post(auth::connections::stripe::login_link_request),
|
||||
)
|
||||
// channels
|
||||
.route("/channels", post(channels::channels::create_request))
|
||||
.route(
|
||||
|
@ -716,17 +702,6 @@ pub fn routes() -> Router {
|
|||
.route("/domains/{id}", get(domains::get_request))
|
||||
.route("/domains/{id}", delete(domains::delete_request))
|
||||
.route("/domains/{id}/data", post(domains::update_data_request))
|
||||
// products
|
||||
.route("/products", get(products::list_request))
|
||||
.route("/products", post(products::create_request))
|
||||
.route("/products/{id}", get(products::get_request))
|
||||
.route("/products/{id}", delete(products::delete_request))
|
||||
.route("/products/{id}/name", post(products::update_name_request))
|
||||
.route(
|
||||
"/products/{id}/description",
|
||||
post(products::update_description_request),
|
||||
)
|
||||
.route("/products/{id}/price", post(products::update_price_request))
|
||||
// letters
|
||||
.route("/letters", post(letters::create_request))
|
||||
.route("/letters/{id}", get(letters::get_request))
|
||||
|
@ -1207,29 +1182,6 @@ pub struct UpdateDomainData {
|
|||
pub data: Vec<(String, DomainData)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateProduct {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub product_type: ProductType,
|
||||
pub price: ProductPrice,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateProductName {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateProductDescription {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateProductPrice {
|
||||
pub price: ProductPrice,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUploadAlt {
|
||||
pub alt: String,
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
use crate::{
|
||||
get_user_from_token,
|
||||
image::{save_webp_buffer, JsonMultipart},
|
||||
routes::{
|
||||
api::v1::{
|
||||
communities::posts::MAXIMUM_FILE_SIZE, CreateProduct, UpdateProductDescription,
|
||||
UpdateProductName, UpdateProductPrice,
|
||||
},
|
||||
pages::PaginatedQuery,
|
||||
},
|
||||
State,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use crate::cookie::CookieJar;
|
||||
use tetratto_core::model::{
|
||||
oauth,
|
||||
products::Product,
|
||||
uploads::{MediaType, MediaUpload},
|
||||
ApiReturn, Error,
|
||||
};
|
||||
|
||||
pub async fn get_request(
|
||||
Path(id): Path<usize>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
match data.get_product_by_id(id).await {
|
||||
Ok(x) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some(x),
|
||||
}),
|
||||
Err(e) => return Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.get_products_by_user(user.id, 12, props.page).await {
|
||||
Ok(x) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some(x),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
JsonMultipart(uploads, req): JsonMultipart<CreateProduct>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
if uploads.len() > 4 {
|
||||
return Json(
|
||||
Error::MiscError("Too many uploads. Please use a maximum of 4".to_string()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut product = Product::new(
|
||||
user.id,
|
||||
req.name,
|
||||
req.description,
|
||||
req.price,
|
||||
req.product_type,
|
||||
);
|
||||
|
||||
// check sizes
|
||||
for img in &uploads {
|
||||
if img.len() > MAXIMUM_FILE_SIZE {
|
||||
return Json(Error::FileTooLarge.into());
|
||||
}
|
||||
}
|
||||
|
||||
// create uploads
|
||||
for _ in 0..uploads.len() {
|
||||
product.uploads.push(
|
||||
match data
|
||||
.create_upload(MediaUpload::new(MediaType::Webp, product.owner))
|
||||
.await
|
||||
{
|
||||
Ok(u) => u.id,
|
||||
Err(e) => return Json(e.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let product_uploads = product.uploads.clone();
|
||||
match data.create_product(product).await {
|
||||
Ok(x) => {
|
||||
// store uploads
|
||||
for (i, upload_id) in product_uploads.iter().enumerate() {
|
||||
let image = match uploads.get(i) {
|
||||
Some(img) => img,
|
||||
None => {
|
||||
if let Err(e) = data.delete_upload(*upload_id).await {
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let upload = match data.get_upload_by_id(*upload_id).await {
|
||||
Ok(u) => u,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
if let Err(e) =
|
||||
save_webp_buffer(&upload.path(&data.0.0).to_string(), image.to_vec(), None)
|
||||
{
|
||||
return Json(Error::MiscError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Product created".to_string(),
|
||||
payload: x.id.to_string(),
|
||||
})
|
||||
}
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_name_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<UpdateProductName>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.update_product_name(id, &user, &req.name).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Product updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_description_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<UpdateProductDescription>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data
|
||||
.update_product_description(id, &user, &req.description)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Product updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_price_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<UpdateProductPrice>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.update_product_price(id, &user, req.price).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Product updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data.delete_product(id, &user).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Product deleted".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
use super::render_error;
|
||||
use crate::{
|
||||
assets::initial_context, get_lang, get_user_from_token, State, routes::pages::PaginatedQuery,
|
||||
};
|
||||
use axum::{
|
||||
extract::Query,
|
||||
response::{Html, IntoResponse},
|
||||
Extension,
|
||||
};
|
||||
use crate::cookie::CookieJar;
|
||||
use tetratto_core::model::Error;
|
||||
|
||||
/// `/settings/seller`
|
||||
pub async fn seller_settings_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let products = match data.0.get_products_by_user(user.id, 12, props.page).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
|
||||
};
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||
|
||||
context.insert("list", &products);
|
||||
context.insert("page", &props.page);
|
||||
|
||||
// return
|
||||
Ok(Html(
|
||||
data.1.render("marketplace/seller.html", &context).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn connection_return_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let mut user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// update user
|
||||
user.seller_data.completed_onboarding = true;
|
||||
if let Err(e) = data
|
||||
.0
|
||||
.update_user_seller_data(user.id, user.seller_data.clone())
|
||||
.await
|
||||
{
|
||||
return Err(Html(render_error(e, &jar, &data, &None).await));
|
||||
}
|
||||
|
||||
// ...
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||
context.insert("connection_type", "return");
|
||||
|
||||
// return
|
||||
Ok(Html(
|
||||
data.1
|
||||
.render("auth/seller_connection.html", &context)
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn connection_reload_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||
context.insert("connection_type", "reload");
|
||||
|
||||
// return
|
||||
Ok(Html(
|
||||
data.1
|
||||
.render("auth/seller_connection.html", &context)
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
|
@ -6,7 +6,6 @@ pub mod forge;
|
|||
pub mod journals;
|
||||
pub mod littleweb;
|
||||
pub mod mail;
|
||||
pub mod marketplace;
|
||||
pub mod misc;
|
||||
pub mod mod_panel;
|
||||
pub mod profile;
|
||||
|
@ -77,14 +76,6 @@ pub fn routes() -> Router {
|
|||
"/auth/connections_link/app/{id}",
|
||||
get(developer::connection_callback_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/connections_link/seller/reload",
|
||||
get(marketplace::connection_reload_request),
|
||||
)
|
||||
.route(
|
||||
"/auth/connections_link/seller/return",
|
||||
get(marketplace::connection_return_request),
|
||||
)
|
||||
// profile
|
||||
.route("/settings", get(profile::settings_request))
|
||||
.route("/@{username}", get(profile::posts_request))
|
||||
|
@ -163,11 +154,6 @@ pub fn routes() -> Router {
|
|||
.route("/domains/{id}", get(littleweb::domain_request))
|
||||
.route("/net", get(littleweb::browser_home_request))
|
||||
.route("/net/{*uri}", get(littleweb::browser_request))
|
||||
// marketplace
|
||||
.route(
|
||||
"/settings/seller",
|
||||
get(marketplace::seller_settings_request),
|
||||
)
|
||||
// mail
|
||||
.route("/mail", get(mail::received_request))
|
||||
.route("/mail/sent", get(mail::sent_request))
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use super::common::NAME_REGEX;
|
||||
use oiseau::cache::Cache;
|
||||
use crate::model::auth::{
|
||||
Achievement, AchievementName, AchievementRarity, Notification, StripeSellerData,
|
||||
UserConnections, ACHIEVEMENTS,
|
||||
Achievement, AchievementName, AchievementRarity, Notification, UserConnections, ACHIEVEMENTS,
|
||||
};
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::oauth::AuthGrant;
|
||||
|
@ -125,11 +124,10 @@ impl DataManager {
|
|||
awaiting_purchase: get!(x->24(i32)) as i8 == 1,
|
||||
was_purchased: get!(x->25(i32)) as i8 == 1,
|
||||
browser_session: get!(x->26(String)),
|
||||
seller_data: serde_json::from_str(&get!(x->27(String)).to_string()).unwrap(),
|
||||
ban_reason: get!(x->28(String)),
|
||||
channel_mutes: serde_json::from_str(&get!(x->29(String)).to_string()).unwrap(),
|
||||
is_deactivated: get!(x->30(i32)) as i8 == 1,
|
||||
ban_expire: get!(x->31(i64)) as usize,
|
||||
ban_reason: get!(x->27(String)),
|
||||
channel_mutes: serde_json::from_str(&get!(x->28(String)).to_string()).unwrap(),
|
||||
is_deactivated: get!(x->29(i32)) as i8 == 1,
|
||||
ban_expire: get!(x->30(i64)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,7 +284,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)",
|
||||
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -315,7 +313,6 @@ impl DataManager {
|
|||
&if data.awaiting_purchase { 1_i32 } else { 0_i32 },
|
||||
&if data.was_purchased { 1_i32 } else { 0_i32 },
|
||||
&data.browser_session,
|
||||
&serde_json::to_string(&data.seller_data).unwrap(),
|
||||
&data.ban_reason,
|
||||
&serde_json::to_string(&data.channel_mutes).unwrap(),
|
||||
&if data.is_deactivated { 1_i32 } else { 0_i32 },
|
||||
|
@ -1058,7 +1055,6 @@ impl DataManager {
|
|||
auto_method!(update_user_achievements(Vec<Achievement>)@get_user_by_id -> "UPDATE users SET achievements = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_invite_code(i64)@get_user_by_id -> "UPDATE users SET invite_code = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_browser_session(&str)@get_user_by_id -> "UPDATE users SET browser_session = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_seller_data(StripeSellerData)@get_user_by_id -> "UPDATE users SET seller_data = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_ban_reason(&str)@get_user_by_id -> "UPDATE users SET ban_reason = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_channel_mutes(Vec<usize>)@get_user_by_id -> "UPDATE users SET channel_mutes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
|
||||
auto_method!(update_user_ban_expire(i64)@get_user_by_id -> "UPDATE users SET ban_expire = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
|
||||
|
|
|
@ -42,7 +42,6 @@ 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();
|
||||
execute!(&conn, common::CREATE_TABLE_APP_DATA).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_LETTERS).unwrap();
|
||||
|
||||
|
|
|
@ -30,6 +30,5 @@ 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");
|
||||
pub const CREATE_TABLE_APP_DATA: &str = include_str!("./sql/create_app_data.sql");
|
||||
pub const CREATE_TABLE_LETTERS: &str = include_str!("./sql/create_letters.sql");
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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,
|
||||
price TEXT NOT NULL,
|
||||
uploads TEXT NOT NULL
|
||||
)
|
|
@ -26,7 +26,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
awaiting_purchase INT NOT NULL,
|
||||
was_purchased INT NOT NULL,
|
||||
browser_session TEXT NOT NULL,
|
||||
seller_data TEXT NOT NULL,
|
||||
ban_reason TEXT NOT NULL,
|
||||
channel_mutes TEXT NOT NULL,
|
||||
is_deactivated INT NOT NULL,
|
||||
|
|
|
@ -29,3 +29,7 @@ ADD COLUMN IF NOT EXISTS topic BIGINT DEFAULT 0;
|
|||
-- users ban_expire
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS ban_expire BIGINT DEFAULT 0;
|
||||
|
||||
-- remove users seller_data
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS seller_data;
|
||||
|
|
|
@ -23,7 +23,6 @@ mod notifications;
|
|||
mod polls;
|
||||
mod pollvotes;
|
||||
mod posts;
|
||||
mod products;
|
||||
mod questions;
|
||||
mod reactions;
|
||||
mod reports;
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
use crate::model::{
|
||||
auth::User,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
products::{Product, ProductPrice},
|
||||
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(),
|
||||
price: serde_json::from_str(&get!(x->8(String))).unwrap(),
|
||||
uploads: 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
|
||||
/// * `batch`
|
||||
/// * `page`
|
||||
pub async fn get_products_by_user(
|
||||
&self,
|
||||
id: usize,
|
||||
batch: usize,
|
||||
page: usize,
|
||||
) -> Result<Vec<Product>> {
|
||||
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 LIMIT {} OFFSET {}",
|
||||
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|
||||
|x| { Self::get_product_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("product".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all products by user.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - the ID of the user to fetch products for
|
||||
pub async fn get_products_by_user_all(&self, id: usize) -> Result<Vec<Product>> {
|
||||
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<Product> {
|
||||
// check values
|
||||
if data.name.trim().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_table_row_count_where("products", &format!("owner = {}", owner.id))
|
||||
.await? as usize;
|
||||
|
||||
if products >= 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(),
|
||||
&serde_json::to_string(&data.price).unwrap(),
|
||||
&serde_json::to_string(&data.uploads).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(())
|
||||
}
|
||||
|
||||
auto_method!(update_product_name(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
||||
auto_method!(update_product_description(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET description = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
||||
auto_method!(update_product_price(ProductPrice)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET price = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
|
||||
}
|
|
@ -79,9 +79,6 @@ pub struct User {
|
|||
/// view pages which require authentication (all `$` routes).
|
||||
#[serde(default)]
|
||||
pub browser_session: String,
|
||||
/// Stripe connected account information (for Tetratto marketplace).
|
||||
#[serde(default)]
|
||||
pub seller_data: StripeSellerData,
|
||||
/// The reason the user was banned.
|
||||
#[serde(default)]
|
||||
pub ban_reason: String,
|
||||
|
@ -355,14 +352,6 @@ pub struct UserSettings {
|
|||
pub forum_signature: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct StripeSellerData {
|
||||
#[serde(default)]
|
||||
pub account_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub completed_onboarding: bool,
|
||||
}
|
||||
|
||||
fn mime_avif() -> String {
|
||||
"image/avif".to_string()
|
||||
}
|
||||
|
@ -407,7 +396,6 @@ impl User {
|
|||
awaiting_purchase: false,
|
||||
was_purchased: false,
|
||||
browser_session: String::new(),
|
||||
seller_data: StripeSellerData::default(),
|
||||
ban_reason: String::new(),
|
||||
channel_mutes: Vec::new(),
|
||||
is_deactivated: false,
|
||||
|
|
|
@ -11,7 +11,6 @@ pub mod mail;
|
|||
pub mod moderation;
|
||||
pub mod oauth;
|
||||
pub mod permissions;
|
||||
pub mod products;
|
||||
pub mod reactions;
|
||||
pub mod requests;
|
||||
pub mod socket;
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Product {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub owner: usize,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub likes: isize,
|
||||
pub dislikes: isize,
|
||||
pub product_type: ProductType,
|
||||
pub price: ProductPrice,
|
||||
/// Optional uploads to accompany the product title and description. Maximum of 4.
|
||||
pub uploads: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ProductType {
|
||||
/// Text + images.
|
||||
Data,
|
||||
/// When a commission product is purchased, the creator will receive a request
|
||||
/// prompting them to respond with text + images.
|
||||
///
|
||||
/// This is the only product type which does not immediately return data to the
|
||||
/// customer, as seller input is required.
|
||||
///
|
||||
/// If the request is deleted, the purchase should be immediately refunded.
|
||||
///
|
||||
/// Commissions are paid beforehand to prevent theft. This means it is vital
|
||||
/// that refunds are enforced.
|
||||
Commission,
|
||||
}
|
||||
|
||||
/// A currency.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Currency {
|
||||
USD,
|
||||
EUR,
|
||||
GBP,
|
||||
}
|
||||
|
||||
impl Display for Currency {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Currency::USD => "$",
|
||||
Currency::EUR => "€",
|
||||
Currency::GBP => "£",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Price in USD. `(dollars, cents)`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProductPrice(u64, u64, Currency);
|
||||
|
||||
impl Display for ProductPrice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{}{}.{}", self.2, self.0, self.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Product {
|
||||
/// Create a new [`Product`].
|
||||
pub fn new(
|
||||
owner: usize,
|
||||
name: String,
|
||||
description: String,
|
||||
price: ProductPrice,
|
||||
r#type: ProductType,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
created: unix_epoch_timestamp(),
|
||||
owner,
|
||||
name,
|
||||
description,
|
||||
likes: 0,
|
||||
dislikes: 0,
|
||||
product_type: r#type,
|
||||
price,
|
||||
uploads: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue