diff --git a/.gitignore b/.gitignore index f5f83f6..7bc86aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target debug/ +.dev diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 8a7eb6e..a54b7a8 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -37,6 +37,7 @@ async-stripe = { version = "0.41.0", features = [ "webhook-events", "billing", "runtime-tokio-hyper", + "connect", ] } emojis = "0.7.0" webp = "0.3.0" diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 81671fe..e4088a1 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -40,7 +40,6 @@ pub const ATTO_JS: &str = include_str!("./public/js/atto.js"); pub const ME_JS: &str = include_str!("./public/js/me.js"); pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); pub const CARP_JS: &str = include_str!("./public/js/carp.js"); -pub const LAYOUT_EDITOR_JS: &str = include_str!("./public/js/layout_editor.js"); pub const PROTO_LINKS_JS: &str = include_str!("./public/js/proto_links.js"); // html diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b4ffbe6..f7f7c06 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -8,6 +8,7 @@ mod routes; mod sanitize; use assets::{init_dirs, write_assets}; +use stripe::Client as StripeClient; use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji}; pub use tetratto_core::*; @@ -27,7 +28,8 @@ use tracing::{Level, info}; use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc}; use tokio::sync::RwLock; -pub(crate) type State = Arc>; +pub(crate) type InnerState = (DataManager, Tera, Client, Option); +pub(crate) type State = Arc>; fn render_markdown(value: &Value, _: &HashMap) -> tera::Result { Ok( @@ -115,6 +117,13 @@ async fn main() { let client = Client::new(); let mut app = Router::new(); + // cretae stripe client + let stripe_client = if let Some(ref stripe) = config.stripe { + Some(StripeClient::new(stripe.secret.clone())) + } else { + None + }; + // add correct routes if var("LITTLEWEB").is_ok() { app = app.merge(routes::lw_routes()); @@ -129,7 +138,12 @@ async fn main() { // add junk app = app - .layer(Extension(Arc::new(RwLock::new((database, tera, client))))) + .layer(Extension(Arc::new(RwLock::new(( + database, + tera, + client, + stripe_client, + ))))) .layer(axum::extract::DefaultBodyLimit::max( var("BODY_LIMIT") .unwrap_or("8388608".to_string()) diff --git a/crates/app/src/public/js/layout_editor.js b/crates/app/src/public/js/layout_editor.js deleted file mode 100644 index 13d3d8b..0000000 --- a/crates/app/src/public/js/layout_editor.js +++ /dev/null @@ -1,762 +0,0 @@ -/// Copy all the fields from one object to another. -function copy_fields(from, to) { - for (const field of Object.entries(from)) { - to[field[0]] = field[1]; - } - - return to; -} - -/// Simple template components. -const COMPONENT_TEMPLATES = { - EMPTY_COMPONENT: { component: "empty", options: {}, children: [] }, - FLEX_DEFAULT: { - component: "flex", - options: { - direction: "row", - gap: "2", - }, - children: [], - }, - FLEX_SIMPLE_ROW: { - component: "flex", - options: { - direction: "row", - gap: "2", - width: "full", - }, - children: [], - }, - FLEX_SIMPLE_COL: { - component: "flex", - options: { - direction: "col", - gap: "2", - width: "full", - }, - children: [], - }, - FLEX_MOBILE_COL: { - component: "flex", - options: { - collapse: "yes", - gap: "2", - width: "full", - }, - children: [], - }, - MARKDOWN_DEFAULT: { - component: "markdown", - options: { - text: "Hello, world!", - }, - }, - MARKDOWN_CARD: { - component: "markdown", - options: { - class: "card w-full", - text: "Hello, world!", - }, - }, -}; - -/// All available components with their label and JSON representation. -const COMPONENTS = [ - [ - "Markdown block", - COMPONENT_TEMPLATES.MARKDOWN_DEFAULT, - [["Card", COMPONENT_TEMPLATES.MARKDOWN_CARD]], - ], - [ - "Flex container", - COMPONENT_TEMPLATES.FLEX_DEFAULT, - [ - ["Simple rows", COMPONENT_TEMPLATES.FLEX_SIMPLE_ROW], - ["Simple columns", COMPONENT_TEMPLATES.FLEX_SIMPLE_COL], - ["Mobile columns", COMPONENT_TEMPLATES.FLEX_MOBILE_COL], - ], - ], - [ - "Profile tabs", - { - component: "tabs", - }, - ], - [ - "Profile feeds", - { - component: "feed", - }, - ], - [ - "Profile banner", - { - component: "banner", - }, - ], - [ - "Question box", - { - component: "ask", - }, - ], - [ - "Name & avatar", - { - component: "name", - }, - ], - [ - "About section", - { - component: "about", - }, - ], - [ - "Action buttons", - { - component: "actions", - }, - ], - [ - "CSS stylesheet", - { - component: "style", - options: { - data: "", - }, - }, - ], -]; - -// preload icons -trigger("app::icon", ["shapes"]); -trigger("app::icon", ["type"]); -trigger("app::icon", ["plus"]); -trigger("app::icon", ["move-up"]); -trigger("app::icon", ["move-down"]); -trigger("app::icon", ["trash"]); -trigger("app::icon", ["arrow-left"]); -trigger("app::icon", ["x"]); - -/// The location of an element as represented by array indexes. -class ElementPointer { - position = []; - - constructor(element) { - if (element) { - const pos = []; - - let target = element; - while (target.parentElement) { - const parent = target.parentElement; - - // push index - pos.push(Array.from(parent.children).indexOf(target) || 0); - - // update target - if (parent.id === "editor") { - break; - } - - target = parent; - } - - this.position = pos.reverse(); // indexes are added in reverse order because of how we traverse - } else { - this.position = []; - } - } - - get() { - return this.position; - } - - resolve(json, minus = 0) { - let out = json; - - if (this.position.length === 1) { - // this is the first element (this.position === [0]) - return out; - } - - const pos = this.position.slice(1, this.position.length); // the first one refers to the root element - - for (let i = 0; i < minus; i++) { - pos.pop(); - } - - for (const idx of pos) { - const child = ((out || { children: [] }).children || [])[idx]; - - if (!child) { - break; - } - - out = child; - } - - return out; - } -} - -/// The layout editor controller. -class LayoutEditor { - element; - json; - tree = ""; - current = { component: "empty" }; - pointer = new ElementPointer(); - - /// Create a new [`LayoutEditor`]. - constructor(element, json) { - this.element = element; - this.json = json; - - if (this.json.json) { - delete this.json.json; - } - - element.addEventListener("click", (e) => this.click(e, this)); - element.addEventListener("mouseover", (e) => { - e.stopImmediatePropagation(); - const ptr = new ElementPointer(e.target); - - if (document.getElementById("position")) { - document.getElementById( - "position", - ).parentElement.style.display = "flex"; - - document.getElementById("position").innerText = ptr - .get() - .join("."); - } - }); - - this.render(); - } - - /// Render layout. - render() { - fetch("/api/v0/auth/render_layout", { - method: "POST", - body: JSON.stringify({ - layout: this.json, - }), - headers: { - "Content-Type": "application/json", - }, - }) - .then((r) => r.json()) - .then((r) => { - this.element.innerHTML = r.block; - this.tree = r.tree; - - if (this.json.component !== "empty") { - // remove all "empty" components (if the root component isn't an empty) - for (const element of document.querySelectorAll( - '[data-component-name="empty"]', - )) { - element.remove(); - } - } - }); - } - - /// Editor clicked. - click(e, self) { - e.stopImmediatePropagation(); - trigger("app::hooks::dropdown.close"); - - const ptr = new ElementPointer(e.target); - self.current = ptr.resolve(self.json); - self.pointer = ptr; - - if (document.getElementById("current_position")) { - document.getElementById( - "current_position", - ).parentElement.style.display = "flex"; - - document.getElementById("current_position").innerText = ptr - .get() - .join("."); - } - - for (const element of document.querySelectorAll( - ".layout_editor_block.active", - )) { - element.classList.remove("active"); - } - - e.target.classList.add("active"); - self.screen("element"); - } - - /// Open sidebar. - open() { - document.getElementById("editor_sidebar").classList.add("open"); - document.getElementById("editor").style.transform = "scale(0.8)"; - } - - /// Close sidebar. - close() { - document.getElementById("editor_sidebar").style.animation = - "0.2s ease-in-out forwards to_left"; - - setTimeout(() => { - document.getElementById("editor_sidebar").classList.remove("open"); - document.getElementById("editor_sidebar").style.animation = - "0.2s ease-in-out forwards from_right"; - }, 250); - - document.getElementById("editor").style.transform = "scale(1)"; - } - - /// Render editor dialog. - screen(page = "element", data = {}) { - this.current.component = this.current.component.toLowerCase(); - - const sidebar = document.getElementById("editor_sidebar"); - sidebar.innerHTML = ""; - - // render page - if ( - page === "add" || - (page === "element" && this.current.component === "empty") - ) { - // add element - sidebar.appendChild( - (() => { - const heading = document.createElement("h3"); - heading.innerText = data.add_title || "Add component"; - return heading; - })(), - ); - - sidebar.appendChild(document.createElement("hr")); - - const container = document.createElement("div"); - container.className = "flex w-full gap-2 flex-wrap"; - - for (const component of data.components || COMPONENTS) { - container.appendChild( - (() => { - const button = document.createElement("button"); - button.classList.add("secondary"); - - trigger("app::icon", [ - data.icon || "shapes", - "icon", - ]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = `${component[0]}${component[2] ? ` (${component[2].length + 1})` : ""}`; - return span; - })(), - ); - - button.addEventListener("click", () => { - if (component[2]) { - // render presets - return this.screen(page, { - back: ["add", {}], - add_title: "Select preset", - components: [ - ["Default", component[1]], - ...component[2], - ], - icon: "type", - }); - } - - // no presets - if ( - page === "element" && - this.current.component === "empty" - ) { - // replace with component - copy_fields(component[1], this.current); - } else { - // add component to children - this.current.children.push( - structuredClone(component[1]), - ); - } - - this.render(); - this.close(); - }); - - return button; - })(), - ); - } - - sidebar.appendChild(container); - } else if (page === "element") { - // edit element - const name = document.createElement("div"); - name.className = "flex flex-col gap-2"; - - name.appendChild( - (() => { - const heading = document.createElement("h3"); - heading.innerText = `Edit ${this.current.component}`; - return heading; - })(), - ); - - name.appendChild( - (() => { - const pos = document.createElement("div"); - pos.className = "notification w-content"; - pos.innerText = this.pointer.get().join("."); - return pos; - })(), - ); - - sidebar.appendChild(name); - sidebar.appendChild(document.createElement("hr")); - - // options - const options = document.createElement("div"); - options.className = "card flex flex-col gap-2 w-full"; - - const add_option = ( - label_text, - name, - valid = [], - input_element = "input", - ) => { - const card = document.createElement("details"); - card.className = "w-full"; - - const summary = document.createElement("summary"); - summary.className = "w-full"; - - const label = document.createElement("label"); - label.setAttribute("for", name); - label.className = "w-full"; - label.innerText = label_text; - label.style.cursor = "pointer"; - - label.addEventListener("click", () => { - // bubble to summary click - summary.click(); - }); - - const input_box = document.createElement("div"); - input_box.style.paddingLeft = "1rem"; - input_box.style.borderLeft = - "solid 2px var(--color-super-lowered)"; - - const input = document.createElement(input_element); - input.id = name; - input.setAttribute("name", name); - input.setAttribute("type", "text"); - - if (input_element === "input") { - input.setAttribute( - "value", - // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code - (this.current.options || {})[name] || "", - ); - } else { - // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code - input.innerHTML = (this.current.options || {})[name] || ""; - } - - // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code - if ((this.current.options || {})[name]) { - // open details if a value is set - card.setAttribute("open", ""); - } - - input.addEventListener("change", (e) => { - if ( - valid.length > 0 && - !valid.includes(e.target.value) && - e.target.value.length > 0 // anything can be set to empty - ) { - alert(`Must be one of: ${JSON.stringify(valid)}`); - return; - } - - if (!this.current.options) { - this.current.options = {}; - } - - this.current.options[name] = - e.target.value === "no" ? "" : e.target.value; - }); - - summary.appendChild(label); - card.appendChild(summary); - input_box.appendChild(input); - card.appendChild(input_box); - options.appendChild(card); - }; - - sidebar.appendChild(options); - - if (this.current.component === "flex") { - add_option("Gap", "gap", ["1", "2", "3", "4"]); - add_option("Direction", "direction", ["row", "col"]); - add_option("Do collapse", "collapse", ["yes", "no"]); - add_option("Width", "width", ["full", "content"]); - add_option("Class name", "class"); - add_option("Unique ID", "id"); - add_option("Style", "style", [], "textarea"); - } else if (this.current.component === "markdown") { - add_option("Content", "text", [], "textarea"); - add_option("Class name", "class"); - } else if (this.current.component === "divider") { - add_option("Class name", "class"); - } else if (this.current.component === "style") { - add_option("Style data", "data", [], "textarea"); - } else { - options.remove(); - } - - // action buttons - const buttons = document.createElement("div"); - buttons.className = "card w-full flex flex-wrap gap-2"; - - if (this.current.component === "flex") { - buttons.appendChild( - (() => { - const button = document.createElement("button"); - - trigger("app::icon", ["plus", "icon"]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Add child"; - return span; - })(), - ); - - button.addEventListener("click", () => { - this.screen("add"); - }); - - return button; - })(), - ); - } - - buttons.appendChild( - (() => { - const button = document.createElement("button"); - - trigger("app::icon", ["move-up", "icon"]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Move up"; - return span; - })(), - ); - - button.addEventListener("click", () => { - const idx = this.pointer.get().pop(); - const parent_ref = this.pointer.resolve( - this.json, - ).children; - - if (parent_ref[idx - 1] === undefined) { - alert("No space to move element."); - return; - } - - const clone = JSON.parse(JSON.stringify(this.current)); - const other_clone = JSON.parse( - JSON.stringify(parent_ref[idx - 1]), - ); - - copy_fields(clone, parent_ref[idx - 1]); // move here to here - copy_fields(other_clone, parent_ref[idx]); // move there to here - - this.close(); - this.render(); - }); - - return button; - })(), - ); - - buttons.appendChild( - (() => { - const button = document.createElement("button"); - - trigger("app::icon", ["move-down", "icon"]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Move down"; - return span; - })(), - ); - - button.addEventListener("click", () => { - const idx = this.pointer.get().pop(); - const parent_ref = this.pointer.resolve( - this.json, - ).children; - - if (parent_ref[idx + 1] === undefined) { - alert("No space to move element."); - return; - } - - const clone = JSON.parse(JSON.stringify(this.current)); - const other_clone = JSON.parse( - JSON.stringify(parent_ref[idx + 1]), - ); - - copy_fields(clone, parent_ref[idx + 1]); // move here to here - copy_fields(other_clone, parent_ref[idx]); // move there to here - - this.close(); - this.render(); - }); - - return button; - })(), - ); - - buttons.appendChild( - (() => { - const button = document.createElement("button"); - button.classList.add("red"); - - trigger("app::icon", ["trash", "icon"]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Delete"; - return span; - })(), - ); - - button.addEventListener("click", async () => { - if ( - !(await trigger("app::confirm", [ - "Are you sure you would like to do this?", - ])) - ) { - return; - } - - if (this.json === this.current) { - // this is the root element; replace with empty - copy_fields( - COMPONENT_TEMPLATES.EMPTY_COMPONENT, - this.current, - ); - } else { - // get parent - const idx = this.pointer.get().pop(); - const ref = this.pointer.resolve(this.json); - // remove element - ref.children.splice(idx, 1); - } - - this.render(); - this.close(); - }); - - return button; - })(), - ); - - sidebar.appendChild(buttons); - } else if (page === "tree") { - sidebar.innerHTML = this.tree; - } - - sidebar.appendChild(document.createElement("hr")); - - const buttons = document.createElement("div"); - buttons.className = "flex gap-2 flex-wrap"; - - if (data.back) { - buttons.appendChild( - (() => { - const button = document.createElement("button"); - button.className = "secondary"; - - trigger("app::icon", ["arrow-left", "icon"]).then( - (icon) => { - button.prepend(icon); - }, - ); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Back"; - return span; - })(), - ); - - button.addEventListener("click", () => { - this.screen(...data.back); - }); - - return button; - })(), - ); - } - - buttons.appendChild( - (() => { - const button = document.createElement("button"); - button.className = "red secondary"; - - trigger("app::icon", ["x", "icon"]).then((icon) => { - button.prepend(icon); - }); - - button.appendChild( - (() => { - const span = document.createElement("span"); - span.innerText = "Close"; - return span; - })(), - ); - - button.addEventListener("click", () => { - this.render(); - this.close(); - }); - - return button; - })(), - ); - - sidebar.appendChild(buttons); - - // ... - this.open(); - } -} - -define("ElementPointer", ElementPointer); -define("LayoutEditor", LayoutEditor); diff --git a/crates/app/src/routes/api/v1/auth/connections/stripe.rs b/crates/app/src/routes/api/v1/auth/connections/stripe.rs index 3a4619e..8343b1b 100644 --- a/crates/app/src/routes/api/v1/auth/connections/stripe.rs +++ b/crates/app/src/routes/api/v1/auth/connections/stripe.rs @@ -1,6 +1,7 @@ -use std::time::Duration; +use std::{str::FromStr, time::Duration}; use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ auth::{User, Notification}, moderation::AuditLogEntry, @@ -8,7 +9,7 @@ use tetratto_core::model::{ ApiReturn, Error, }; use stripe::{EventObject, EventType}; -use crate::State; +use crate::{get_user_from_token, State}; pub async fn stripe_webhook( Extension(data): Extension, @@ -320,3 +321,102 @@ pub async fn stripe_webhook( payload: (), }) } + +pub async fn onboarding_account_link_request( + jar: CookieJar, + Extension(data): Extension, +) -> 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()), + }; + + 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, +) -> 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()), + }; + + 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()), + } +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index b3496db..164b17f 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -512,6 +512,14 @@ 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), + ) // channels .route("/channels", post(channels::channels::create_request)) .route( diff --git a/crates/app/src/routes/assets.rs b/crates/app/src/routes/assets.rs index 2aa1bc5..d7843bd 100644 --- a/crates/app/src/routes/assets.rs +++ b/crates/app/src/routes/assets.rs @@ -19,5 +19,4 @@ serve_asset!(atto_js_request: ATTO_JS("text/javascript")); serve_asset!(me_js_request: ME_JS("text/javascript")); serve_asset!(streams_js_request: STREAMS_JS("text/javascript")); serve_asset!(carp_js_request: CARP_JS("text/javascript")); -serve_asset!(layout_editor_js_request: LAYOUT_EDITOR_JS("text/javascript")); serve_asset!(proto_links_request: PROTO_LINKS_JS("text/javascript")); diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs index 0872632..e0fa067 100644 --- a/crates/app/src/routes/mod.rs +++ b/crates/app/src/routes/mod.rs @@ -20,10 +20,6 @@ pub fn routes(config: &Config) -> Router { .route("/js/me.js", get(assets::me_js_request)) .route("/js/streams.js", get(assets::streams_js_request)) .route("/js/carp.js", get(assets::carp_js_request)) - .route( - "/js/layout_editor.js", - get(assets::layout_editor_js_request), - ) .route("/js/proto_links.js", get(assets::proto_links_request)) .nest_service( "/public", diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 6ce6318..ed513f9 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -17,11 +17,10 @@ use axum::{ use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::{ - DataManager, model::{Error, auth::User}, }; -use crate::{assets::initial_context, get_lang}; +use crate::{assets::initial_context, get_lang, InnerState}; pub fn routes() -> Router { Router::new() @@ -156,7 +155,7 @@ pub fn lw_routes() -> Router { pub async fn render_error( e: Error, jar: &CookieJar, - data: &(DataManager, tera::Tera, reqwest::Client), + data: &InnerState, user: &Option, ) -> String { let lang = get_lang!(jar, data.0); diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 44d1257..d695c39 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -173,6 +173,8 @@ pub struct ConnectionsConfig { /// - Use testing card numbers: #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct StripeConfig { + /// Your Stripe API secret. + pub secret: String, /// Payment links from the Stripe dashboard. /// /// 1. Create a product and set the price for your membership diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 88ef32e..9bfdd36 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -1,7 +1,8 @@ use super::common::NAME_REGEX; use oiseau::cache::Cache; use crate::model::auth::{ - Achievement, AchievementName, AchievementRarity, Notification, UserConnections, ACHIEVEMENTS, + Achievement, AchievementName, AchievementRarity, Notification, StripeSellerData, + UserConnections, ACHIEVEMENTS, }; use crate::model::moderation::AuditLogEntry; use crate::model::oauth::AuthGrant; @@ -117,6 +118,7 @@ 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(), } } @@ -273,7 +275,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)", + "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)", params![ &(data.id as i64), &(data.created as i64), @@ -302,6 +304,7 @@ 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(), ] ); @@ -997,6 +1000,7 @@ impl DataManager { auto_method!(update_user_achievements(Vec)@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!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index a97b1fd..1119d8b 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -80,6 +80,9 @@ 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, } pub type UserConnections = @@ -327,6 +330,12 @@ pub struct UserSettings { pub private_biography: String, } +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct StripeSellerData { + #[serde(default)] + pub account_id: Option, +} + fn mime_avif() -> String { "image/avif".to_string() } @@ -371,6 +380,7 @@ impl User { awaiting_purchase: false, was_purchased: false, browser_session: String::new(), + seller_data: StripeSellerData::default(), } } diff --git a/sql_changes/users_seller_data.sql b/sql_changes/users_seller_data.sql new file mode 100644 index 0000000..fa8a1f0 --- /dev/null +++ b/sql_changes/users_seller_data.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN seller_data TEXT NOT NULL DEFAULT '{}';