add: user seller_data
This commit is contained in:
parent
fdaa81422a
commit
e4468e4768
14 changed files with 150 additions and 777 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
debug/
|
debug/
|
||||||
|
.dev
|
||||||
|
|
|
@ -37,6 +37,7 @@ async-stripe = { version = "0.41.0", features = [
|
||||||
"webhook-events",
|
"webhook-events",
|
||||||
"billing",
|
"billing",
|
||||||
"runtime-tokio-hyper",
|
"runtime-tokio-hyper",
|
||||||
|
"connect",
|
||||||
] }
|
] }
|
||||||
emojis = "0.7.0"
|
emojis = "0.7.0"
|
||||||
webp = "0.3.0"
|
webp = "0.3.0"
|
||||||
|
|
|
@ -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 ME_JS: &str = include_str!("./public/js/me.js");
|
||||||
pub const STREAMS_JS: &str = include_str!("./public/js/streams.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 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");
|
pub const PROTO_LINKS_JS: &str = include_str!("./public/js/proto_links.js");
|
||||||
|
|
||||||
// html
|
// html
|
||||||
|
|
|
@ -8,6 +8,7 @@ mod routes;
|
||||||
mod sanitize;
|
mod sanitize;
|
||||||
|
|
||||||
use assets::{init_dirs, write_assets};
|
use assets::{init_dirs, write_assets};
|
||||||
|
use stripe::Client as StripeClient;
|
||||||
use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji};
|
use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji};
|
||||||
pub use tetratto_core::*;
|
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 std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
pub(crate) type State = Arc<RwLock<(DataManager, Tera, Client)>>;
|
pub(crate) type InnerState = (DataManager, Tera, Client, Option<StripeClient>);
|
||||||
|
pub(crate) type State = Arc<RwLock<InnerState>>;
|
||||||
|
|
||||||
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
Ok(
|
Ok(
|
||||||
|
@ -115,6 +117,13 @@ async fn main() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let mut app = Router::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
|
// add correct routes
|
||||||
if var("LITTLEWEB").is_ok() {
|
if var("LITTLEWEB").is_ok() {
|
||||||
app = app.merge(routes::lw_routes());
|
app = app.merge(routes::lw_routes());
|
||||||
|
@ -129,7 +138,12 @@ async fn main() {
|
||||||
|
|
||||||
// add junk
|
// add junk
|
||||||
app = app
|
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(
|
.layer(axum::extract::DefaultBodyLimit::max(
|
||||||
var("BODY_LIMIT")
|
var("BODY_LIMIT")
|
||||||
.unwrap_or("8388608".to_string())
|
.unwrap_or("8388608".to_string())
|
||||||
|
|
|
@ -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);
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::time::Duration;
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
auth::{User, Notification},
|
auth::{User, Notification},
|
||||||
moderation::AuditLogEntry,
|
moderation::AuditLogEntry,
|
||||||
|
@ -8,7 +9,7 @@ use tetratto_core::model::{
|
||||||
ApiReturn, Error,
|
ApiReturn, Error,
|
||||||
};
|
};
|
||||||
use stripe::{EventObject, EventType};
|
use stripe::{EventObject, EventType};
|
||||||
use crate::State;
|
use crate::{get_user_from_token, State};
|
||||||
|
|
||||||
pub async fn stripe_webhook(
|
pub async fn stripe_webhook(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
|
@ -320,3 +321,102 @@ pub async fn stripe_webhook(
|
||||||
payload: (),
|
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()),
|
||||||
|
};
|
||||||
|
|
||||||
|
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()),
|
||||||
|
};
|
||||||
|
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -512,6 +512,14 @@ pub fn routes() -> Router {
|
||||||
"/service_hooks/stripe",
|
"/service_hooks/stripe",
|
||||||
post(auth::connections::stripe::stripe_webhook),
|
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
|
// channels
|
||||||
.route("/channels", post(channels::channels::create_request))
|
.route("/channels", post(channels::channels::create_request))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -19,5 +19,4 @@ serve_asset!(atto_js_request: ATTO_JS("text/javascript"));
|
||||||
serve_asset!(me_js_request: ME_JS("text/javascript"));
|
serve_asset!(me_js_request: ME_JS("text/javascript"));
|
||||||
serve_asset!(streams_js_request: STREAMS_JS("text/javascript"));
|
serve_asset!(streams_js_request: STREAMS_JS("text/javascript"));
|
||||||
serve_asset!(carp_js_request: CARP_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"));
|
serve_asset!(proto_links_request: PROTO_LINKS_JS("text/javascript"));
|
||||||
|
|
|
@ -20,10 +20,6 @@ pub fn routes(config: &Config) -> Router {
|
||||||
.route("/js/me.js", get(assets::me_js_request))
|
.route("/js/me.js", get(assets::me_js_request))
|
||||||
.route("/js/streams.js", get(assets::streams_js_request))
|
.route("/js/streams.js", get(assets::streams_js_request))
|
||||||
.route("/js/carp.js", get(assets::carp_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))
|
.route("/js/proto_links.js", get(assets::proto_links_request))
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/public",
|
"/public",
|
||||||
|
|
|
@ -17,11 +17,10 @@ use axum::{
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tetratto_core::{
|
use tetratto_core::{
|
||||||
DataManager,
|
|
||||||
model::{Error, auth::User},
|
model::{Error, auth::User},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{assets::initial_context, get_lang};
|
use crate::{assets::initial_context, get_lang, InnerState};
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
@ -156,7 +155,7 @@ pub fn lw_routes() -> Router {
|
||||||
pub async fn render_error(
|
pub async fn render_error(
|
||||||
e: Error,
|
e: Error,
|
||||||
jar: &CookieJar,
|
jar: &CookieJar,
|
||||||
data: &(DataManager, tera::Tera, reqwest::Client),
|
data: &InnerState,
|
||||||
user: &Option<User>,
|
user: &Option<User>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
|
|
|
@ -173,6 +173,8 @@ pub struct ConnectionsConfig {
|
||||||
/// - Use testing card numbers: <https://docs.stripe.com/testing?testing-method=card-numbers#visa>
|
/// - Use testing card numbers: <https://docs.stripe.com/testing?testing-method=card-numbers#visa>
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||||
pub struct StripeConfig {
|
pub struct StripeConfig {
|
||||||
|
/// Your Stripe API secret.
|
||||||
|
pub secret: String,
|
||||||
/// Payment links from the Stripe dashboard.
|
/// Payment links from the Stripe dashboard.
|
||||||
///
|
///
|
||||||
/// 1. Create a product and set the price for your membership
|
/// 1. Create a product and set the price for your membership
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use super::common::NAME_REGEX;
|
use super::common::NAME_REGEX;
|
||||||
use oiseau::cache::Cache;
|
use oiseau::cache::Cache;
|
||||||
use crate::model::auth::{
|
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::moderation::AuditLogEntry;
|
||||||
use crate::model::oauth::AuthGrant;
|
use crate::model::oauth::AuthGrant;
|
||||||
|
@ -117,6 +118,7 @@ impl DataManager {
|
||||||
awaiting_purchase: get!(x->24(i32)) as i8 == 1,
|
awaiting_purchase: get!(x->24(i32)) as i8 == 1,
|
||||||
was_purchased: get!(x->25(i32)) as i8 == 1,
|
was_purchased: get!(x->25(i32)) as i8 == 1,
|
||||||
browser_session: get!(x->26(String)),
|
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!(
|
let res = execute!(
|
||||||
&conn,
|
&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![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
@ -302,6 +304,7 @@ impl DataManager {
|
||||||
&if data.awaiting_purchase { 1_i32 } else { 0_i32 },
|
&if data.awaiting_purchase { 1_i32 } else { 0_i32 },
|
||||||
&if data.was_purchased { 1_i32 } else { 0_i32 },
|
&if data.was_purchased { 1_i32 } else { 0_i32 },
|
||||||
&data.browser_session,
|
&data.browser_session,
|
||||||
|
&serde_json::to_string(&data.seller_data).unwrap(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -997,6 +1000,7 @@ 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_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_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_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!(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);
|
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);
|
||||||
|
|
|
@ -80,6 +80,9 @@ pub struct User {
|
||||||
/// view pages which require authentication (all `$` routes).
|
/// view pages which require authentication (all `$` routes).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub browser_session: String,
|
pub browser_session: String,
|
||||||
|
/// Stripe connected account information (for Tetratto marketplace).
|
||||||
|
#[serde(default)]
|
||||||
|
pub seller_data: StripeSellerData,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type UserConnections =
|
pub type UserConnections =
|
||||||
|
@ -327,6 +330,12 @@ pub struct UserSettings {
|
||||||
pub private_biography: String,
|
pub private_biography: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
|
pub struct StripeSellerData {
|
||||||
|
#[serde(default)]
|
||||||
|
pub account_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn mime_avif() -> String {
|
fn mime_avif() -> String {
|
||||||
"image/avif".to_string()
|
"image/avif".to_string()
|
||||||
}
|
}
|
||||||
|
@ -371,6 +380,7 @@ impl User {
|
||||||
awaiting_purchase: false,
|
awaiting_purchase: false,
|
||||||
was_purchased: false,
|
was_purchased: false,
|
||||||
browser_session: String::new(),
|
browser_session: String::new(),
|
||||||
|
seller_data: StripeSellerData::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
sql_changes/users_seller_data.sql
Normal file
2
sql_changes/users_seller_data.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN seller_data TEXT NOT NULL DEFAULT '{}';
|
Loading…
Add table
Add a link
Reference in a new issue