Compare commits

...
Author SHA1 Message Date
Brandon Thomas a569947608 cleanup 2026-03-05 21:20:22 -05:00
Brandon Thomas 9d66a880f9 codegen 2026-03-05 21:02:56 -05:00
Brandon Thomas 7c2b0f310e Handler to record tracking data 2026-03-05 21:01:51 -05:00
Brandon Thomas d374317a77 one more alter statement for tracking (this is to thwart abuse) (#1106)
Without these I won't know if someone is spamming us with fraudulent
data
2026-03-05 20:20:18 -05:00
Brandon Thomas f342befb0b user signup referrer (#1105)
see last PR
2026-03-05 20:07:38 -05:00
Brandon Thomas 13d1afd88d schema change: find out how users find us (#1104)
We're totally blind. Google Analytics helps a little bit, but we need to
know which communities find ArtCraft the most valuable. (Am I wasting my
time posting to Reddit?)
2026-03-05 19:56:55 -05:00
bflatastic 135dea926b viewer 3d fixed on artcraft-website media page 2026-03-06 02:08:51 +07:00
Brandon Thomas 4f68100c8f support handler to add balances to accounts (#1103) 2026-03-05 10:35:26 -05:00
Brandon Thomas 908f5cd3d6 support endpoints for looking up payments entities (#1102)
This will make support and issuing refunds easier
2026-03-05 10:01:15 -05:00
Brandon Thomas 5a57e48fcb Bt modify mod user lookup (#1101) 2026-03-05 09:38:00 -05:00
Brandon Thomas 69666c2646 rename pool acquire variant to be better (#1100) 2026-03-05 09:20:29 -05:00
Brandon Thomas 4ed8b2111b list wallet ledger entries for support (#1099) 2026-03-05 09:15:52 -05:00
Brandon Thomas d0fe9e1676 list user wallets for support (#1098) 2026-03-05 08:51:38 -05:00
Brandon Thomas d455056c61 moderator list user jobs handler (for oncall support) (#1097)
Helps us quickly find issues with user jobs so we can unstick jobs,
issue refunds, etc.
2026-03-05 08:16:25 -05:00
Brandon Thomas f17388c9de more moderator handlers (#1096) 2026-03-05 07:18:27 -05:00
Brandon Thomas 7665a07227 admin dashboard cors (#1095) 2026-03-05 06:51:42 -05:00
Brandon Thomas 6dc55c1b62 moderator user lookup (#1094) 2026-03-05 06:40:40 -05:00
Brandon Thomas 0d565563b2 add missing api docs (#1093) 2026-03-05 05:55:03 -05:00
99 changed files with 3411 additions and 28 deletions
@@ -0,0 +1,102 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n j.status as `job_status: enums::common::job_status_plus::JobStatusPlus`,\n j.failure_reason as job_failure_reason,\n wle.credits_delta,\n wle.maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,\n j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`\nFROM generic_inference_jobs as j\nLEFT OUTER JOIN users as u\n ON j.maybe_creator_user_token = u.token\nLEFT OUTER JOIN wallet_ledger_entries as wle\n ON wle.maybe_entity_ref = j.token\nWHERE u.token = ?\nORDER BY j.id DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
},
{
"ordinal": 1,
"name": "job_failure_reason",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 2,
"name": "credits_delta",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 3,
"name": "maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "on_success_result_media_token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
true,
true,
true,
false,
true,
true
]
},
"hash": "0757d81ba153b2ad2df25df3dd92b655ef08b95f5f83251bf3573f9be3a9f8d1"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ",
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 22
"Right": 23
},
"nullable": []
},
"hash": "56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740"
"hash": "08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c"
}
@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n user_subscriptions.subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n user_subscriptions.maybe_stripe_subscription_id,\n users.token as `user_token: tokens::tokens::users::UserToken`,\n users.email_address,\n users.username,\n users.display_name\nFROM user_subscriptions\nINNER JOIN users ON users.token = user_subscriptions.user_token\nWHERE\n user_subscriptions.maybe_stripe_customer_id = ?\n AND user_subscriptions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nORDER BY user_subscriptions.created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subscription_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_stripe_subscription_id",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 2,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 5,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
false,
false,
false
]
},
"hash": "3390084f700ef2444691a02c691a59ccdd66bc8b096547e89c68b58b04dd44e4"
}
@@ -0,0 +1,102 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n w.token as `token: tokens::tokens::wallets::WalletToken`,\n w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n w.owner_user_token as `owner_user_token: tokens::tokens::users::UserToken`,\n w.banked_credits as `banked_credits: u32`,\n w.monthly_credits as `monthly_credits: u32`,\n w.version,\n w.created_at as `created_at: DateTime<Utc>`,\n w.updated_at as `updated_at: DateTime<Utc>`\nFROM wallets as w\nWHERE w.token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::wallets::WalletToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "wallet_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "owner_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "banked_credits: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "monthly_credits: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "version",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "35b09082ebb923c8c3191eaee8553b7c3badbd9152ee43999f7c8ff1a6410fe9"
}
@@ -0,0 +1,91 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n w.token as `token: tokens::tokens::wallets::WalletToken`,\n w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n w.banked_credits as `banked_credits: u32`,\n w.monthly_credits as `monthly_credits: u32`,\n w.version,\n w.created_at as `created_at: DateTime<Utc>`,\n w.updated_at as `updated_at: DateTime<Utc>`\nFROM wallets as w\nWHERE w.owner_user_token = ?\nORDER BY w.id DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::wallets::WalletToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "wallet_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "banked_credits: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "monthly_credits: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "version",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 5,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 6,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "69b6e933ad400bc574204de301dde6748d2a1723f47c12e86afc05033a60d3a2"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,\n entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n maybe_entity_ref,\n credits_delta,\n banked_credits_before as `banked_credits_before: u32`,\n banked_credits_after as `banked_credits_after: u32`,\n monthly_credits_before as `monthly_credits_before: u32`,\n monthly_credits_after as `monthly_credits_after: u32`,\n created_at as `created_at: DateTime<Utc>`,\n is_refunded as `is_refunded: bool`,\n maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\nFROM wallet_ledger_entries\nWHERE wallet_token = ?\nORDER BY id DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "wallet_token: tokens::tokens::wallets::WalletToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_entity_ref",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "credits_delta",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 5,
"name": "banked_credits_before: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 6,
"name": "banked_credits_after: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 7,
"name": "monthly_credits_before: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "monthly_credits_after: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "is_refunded: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 11,
"name": "maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "7f874f4ac43d8c66e3ea8cb2fb56f57b93047355dd9faee4df48799f6d1be00a"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO web_referrals (\n url,\n domain,\n utm_source,\n utm_medium,\n utm_campaign,\n ip_address,\n anonymous_visitor_token\n ) VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "80f73702374593c141d183d86bd8af73d37fd7118914d0ba12a02f53c1b84c78"
}
@@ -0,0 +1,201 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: tokens::tokens::users::UserToken`,\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n is_temporary as `is_temporary: bool`,\n username_is_not_customized as `username_is_not_customized: bool`,\n email_address,\n email_confirmed as `email_confirmed: bool`,\n email_is_synthetic as `email_is_synthetic: bool`,\n is_without_password as `is_without_password: bool`,\n ip_address_creation,\n ip_address_last_login,\n maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n email_gravatar_hash,\n is_banned as `is_banned: bool`,\n created_at,\n updated_at\nFROM users\nWHERE\n users.username = ?\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "username_is_generated: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 4,
"name": "is_temporary: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "username_is_not_customized: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "email_confirmed: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "email_is_synthetic: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_without_password: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "ip_address_last_login",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 12,
"name": "maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "email_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 14,
"name": "is_banned: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 16,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "97599bdc5467b8750d2546cc6f78457c26d93c3be52921ad9b3531fd88498194"
}
@@ -0,0 +1,135 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n maybe_entity_ref,\n credits_delta,\n banked_credits_before as `banked_credits_before: u32`,\n banked_credits_after as `banked_credits_after: u32`,\n monthly_credits_before as `monthly_credits_before: u32`,\n monthly_credits_after as `monthly_credits_after: u32`,\n is_refunded as `is_refunded: bool`,\n maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n created_at as `created_at: DateTime<Utc>`\nFROM wallet_ledger_entries\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "maybe_entity_ref",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "credits_delta",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 4,
"name": "banked_credits_before: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "banked_credits_after: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 6,
"name": "monthly_credits_before: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 7,
"name": "monthly_credits_after: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "is_refunded: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "9c28e9f8f163407755acaca1fb4a72849679b6bf98b8c87e1761200300111232"
}
@@ -0,0 +1,201 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: tokens::tokens::users::UserToken`,\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n is_temporary as `is_temporary: bool`,\n username_is_not_customized as `username_is_not_customized: bool`,\n email_address,\n email_confirmed as `email_confirmed: bool`,\n email_is_synthetic as `email_is_synthetic: bool`,\n is_without_password as `is_without_password: bool`,\n ip_address_creation,\n ip_address_last_login,\n maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n email_gravatar_hash,\n is_banned as `is_banned: bool`,\n created_at,\n updated_at\nFROM users\nWHERE\n users.email_address = ?\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "username_is_generated: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 4,
"name": "is_temporary: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "username_is_not_customized: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "email_confirmed: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "email_is_synthetic: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_without_password: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "ip_address_last_login",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 12,
"name": "maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "email_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 14,
"name": "is_banned: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 16,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "d023eb33c9a06f8b07b1e5d1e255dbb730244bbd764a49d519473e50751a3ddc"
}
@@ -0,0 +1,201 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: tokens::tokens::users::UserToken`,\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n is_temporary as `is_temporary: bool`,\n username_is_not_customized as `username_is_not_customized: bool`,\n email_address,\n email_confirmed as `email_confirmed: bool`,\n email_is_synthetic as `email_is_synthetic: bool`,\n is_without_password as `is_without_password: bool`,\n ip_address_creation,\n ip_address_last_login,\n maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n email_gravatar_hash,\n is_banned as `is_banned: bool`,\n created_at,\n updated_at\nFROM users\nWHERE\n users.token = ?\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "username_is_generated: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 4,
"name": "is_temporary: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "username_is_not_customized: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "email_confirmed: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "email_is_synthetic: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_without_password: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "ip_address_last_login",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 12,
"name": "maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "email_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 14,
"name": "is_banned: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 16,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "fd92483d9f1849d5b7f04076446fa98688fec46bb1cba20d005bc8dad4d1c093"
}
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
maybe_referral_url;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
maybe_referral_url VARCHAR(255) DEFAULT NULL;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE web_referrals;
@@ -0,0 +1,27 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Records of web referrals
CREATE TABLE web_referrals (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- The referral URL
url VARCHAR(255) NOT NULL,
-- The parsed out referral domain
domain VARCHAR(255) DEFAULT NULL,
utm_source VARCHAR(32) DEFAULT NULL,
utm_medium VARCHAR(32) DEFAULT NULL,
utm_campaign VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE web_referrals
DROP COLUMN ip_address,
DROP COLUMN anonymous_visitor_token;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE web_referrals
ADD COLUMN ip_address VARCHAR(40) NOT NULL,
ADD COLUMN anonymous_visitor_token VARCHAR(32) DEFAULT NULL;
@@ -220,6 +220,9 @@ CREATE TABLE users (
-- statistical evaluation.)
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false,
-- The referral URL the user arrived from when signing up.
maybe_referral_url VARCHAR(255) DEFAULT NULL,
-- ========== MODERATION DETAILS ==========
-- Different than deleted.
@@ -0,0 +1,31 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Records of web referrals
CREATE TABLE web_referrals (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- The referral URL
url VARCHAR(255) NOT NULL,
-- The parsed out referral domain
domain VARCHAR(255) DEFAULT NULL,
utm_source VARCHAR(32) DEFAULT NULL,
utm_medium VARCHAR(32) DEFAULT NULL,
utm_campaign VARCHAR(255) DEFAULT NULL,
ip_address VARCHAR(40) NOT NULL,
anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -4,8 +4,10 @@ pub mod credits;
pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod moderation;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod users;
pub mod utils;
pub mod utils;
pub mod web_referrals;
@@ -0,0 +1 @@
pub mod user;
@@ -0,0 +1,33 @@
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use enums::common::job_status_plus::JobStatusPlus;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use utoipa::ToSchema;
pub const LIST_USER_JOBS_PATH: &str = "/v1/moderation/jobs/user/{user_token}/list";
#[derive(Deserialize, ToSchema)]
pub struct ListUserJobsPathInfo {
pub user_token: UserToken,
}
#[derive(Serialize, ToSchema)]
pub struct ListUserJobsResponse {
pub success: bool,
pub jobs: Vec<ListUserJobsEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ListUserJobsEntry {
pub job_status: JobStatusPlus,
pub job_failure_reason: Option<String>,
pub credits_delta: Option<i32>,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
pub on_success_result_media_token: Option<MediaFileToken>,
pub job_token: InferenceJobToken,
pub wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub wallet_ledger_entry_type: Option<WalletLedgerEntryType>,
}
@@ -0,0 +1 @@
pub mod list_user_jobs;
@@ -0,0 +1,4 @@
pub mod jobs;
pub mod user;
pub mod wallet_ledger_entries;
pub mod wallets;
@@ -0,0 +1,2 @@
pub mod user_lookup;
pub mod user_lookup_by_stripe_customer_id;
@@ -0,0 +1,39 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
pub const MODERATOR_USER_LOOKUP_PATH: &str = "/v1/moderation/users/lookup";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorUserLookupRequest {
pub search: String,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserLookupSuccessResponse {
pub success: bool,
pub maybe_user: Option<ModeratorUserLookupUserDetails>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserLookupUserDetails {
pub token: UserToken,
pub username: String,
pub display_name: String,
pub username_is_generated: bool,
pub is_temporary: bool,
pub username_is_not_customized: bool,
pub email_address: String,
pub email_confirmed: bool,
pub email_is_synthetic: bool,
pub is_without_password: bool,
pub ip_address_creation: String,
pub ip_address_last_login: String,
pub maybe_avatar_media_file_token: Option<MediaFileToken>,
pub email_gravatar_hash: String,
pub is_banned: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,27 @@
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
pub const MODERATOR_USER_LOOKUP_BY_STRIPE_CUSTOMER_ID_PATH: &str = "/v1/moderation/users/lookup_by_stripe_customer_id";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorUserLookupByStripeCustomerIdRequest {
pub stripe_customer_id: String,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserLookupByStripeCustomerIdResponse {
pub success: bool,
pub users: Vec<ModeratorUserLookupByStripeCustomerIdEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserLookupByStripeCustomerIdEntry {
pub subscription_namespace: PaymentsNamespace,
pub maybe_stripe_subscription_id: Option<String>,
pub token: UserToken,
pub email_address: String,
pub username: String,
pub display_name: String,
}
@@ -0,0 +1,35 @@
use chrono::{DateTime, Utc};
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
pub const LIST_WALLET_LEDGER_ENTRIES_BY_WALLET_PATH: &str = "/v1/moderation/wallet_ledger_entries/wallet/{wallet_token}/list";
#[derive(Deserialize, ToSchema)]
pub struct ListWalletLedgerEntriesByWalletPathInfo {
pub wallet_token: WalletToken,
}
#[derive(Serialize, ToSchema)]
pub struct ListWalletLedgerEntriesByWalletResponse {
pub success: bool,
pub entries: Vec<ListWalletLedgerEntriesByWalletEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ListWalletLedgerEntriesByWalletEntry {
pub token: WalletLedgerEntryToken,
pub wallet_token: WalletToken,
pub entry_type: WalletLedgerEntryType,
pub maybe_entity_ref: Option<String>,
pub credits_delta: i32,
pub banked_credits_before: u32,
pub banked_credits_after: u32,
pub monthly_credits_before: u32,
pub monthly_credits_after: u32,
pub created_at: DateTime<Utc>,
pub is_refunded: bool,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
}
@@ -0,0 +1,2 @@
pub mod list_wallet_ledger_entries_by_wallet;
pub mod moderator_get_wallet_ledger_entry;
@@ -0,0 +1,33 @@
use chrono::{DateTime, Utc};
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use utoipa::ToSchema;
pub const MODERATOR_GET_WALLET_LEDGER_ENTRY_PATH: &str = "/v1/moderation/wallet_ledger_entry/{wallet_ledger_entry_token}";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorGetWalletLedgerEntryPathInfo {
pub wallet_ledger_entry_token: WalletLedgerEntryToken,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetWalletLedgerEntryResponse {
pub success: bool,
pub maybe_entry: Option<ModeratorGetWalletLedgerEntryDetails>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetWalletLedgerEntryDetails {
pub token: WalletLedgerEntryToken,
pub entry_type: WalletLedgerEntryType,
pub maybe_entity_ref: Option<String>,
pub credits_delta: i32,
pub banked_credits_before: u32,
pub banked_credits_after: u32,
pub monthly_credits_before: u32,
pub monthly_credits_after: u32,
pub is_refunded: bool,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
pub created_at: DateTime<Utc>,
}
@@ -0,0 +1,30 @@
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
pub const LIST_USER_WALLETS_PATH: &str = "/v1/moderation/wallets/user/{user_token}/list";
#[derive(Deserialize, ToSchema)]
pub struct ListUserWalletsPathInfo {
pub user_token: UserToken,
}
#[derive(Serialize, ToSchema)]
pub struct ListUserWalletsResponse {
pub success: bool,
pub wallets: Vec<ListUserWalletsEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ListUserWalletsEntry {
pub token: WalletToken,
pub wallet_namespace: PaymentsNamespace,
pub banked_credits: u32,
pub monthly_credits: u32,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,3 @@
pub mod list_user_wallets;
pub mod moderator_add_banked_balance_to_wallet;
pub mod moderator_get_wallet;
@@ -0,0 +1,20 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
pub const MODERATOR_ADD_BANKED_BALANCE_TO_WALLET_PATH: &str = "/v1/moderation/wallet/{wallet_token}/add_banked_balance";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorAddBankedBalanceToWalletPathInfo {
pub wallet_token: WalletToken,
}
#[derive(Deserialize, ToSchema)]
pub struct ModeratorAddBankedBalanceToWalletRequest {
pub credits: u16,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorAddBankedBalanceToWalletResponse {
pub success: bool,
}
@@ -0,0 +1,31 @@
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
pub const MODERATOR_GET_WALLET_PATH: &str = "/v1/moderation/wallet/{wallet_token}";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorGetWalletPathInfo {
pub wallet_token: WalletToken,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetWalletResponse {
pub success: bool,
pub maybe_wallet: Option<ModeratorGetWalletDetails>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetWalletDetails {
pub token: WalletToken,
pub wallet_namespace: PaymentsNamespace,
pub owner_user_token: UserToken,
pub banked_credits: u32,
pub monthly_credits: u32,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -11,6 +11,11 @@ pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
pub plan: Option<ArtcraftSubscriptionSlug>,
pub cadence: Option<PlanBillingCadence>,
/// Optional: The referral URL the user arrived from when signing up.
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
/// If the browser doesn't send this parameter, we'll try the `referer` header.
pub maybe_referral_url: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const LOG_WEB_REFERRAL_URL_PATH: &str = "/v1/web_referrals/record";
#[derive(Deserialize, ToSchema)]
pub struct LogWebReferralRequest {
/// Optional: The referral URL the user arrived from when signing up.
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
/// If the browser doesn't send this parameter, we'll try the `referer` header.
pub maybe_referral_url: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct LogWebReferralResponse {
pub success: bool,
}
@@ -0,0 +1 @@
pub mod log_web_referral;
@@ -62,6 +62,7 @@ async fn seed_user(
password_hash: &password_hash,
ip_address: "127.0.0.1",
maybe_source: None,
maybe_referral_url: None,
maybe_user_token: maybe_user_token.as_ref(),
}).await.map_err(|err| anyhow!("err: {:?}", err))?;
@@ -0,0 +1,10 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
pub fn add_artcraft_admin_dashboard(cors: Cors, _is_production: bool) -> Cors {
cors
.allowed_origin("https://artcraft-dashboard.netlify.app")
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "artcraft-dashboard.netlify.app")
})
}
@@ -1,3 +1,4 @@
pub (crate) mod artcraft_admin_dashboard;
pub (crate) mod artcraft_2d;
pub (crate) mod artcraft_3d;
pub (crate) mod artcraft_website;
@@ -1,6 +1,7 @@
use actix_cors::Cors;
use log::info;
use crate::configs::artcraft_admin_dashboard::add_artcraft_admin_dashboard;
use crate::configs::artcraft_2d::add_artcraft_2d;
use crate::configs::artcraft_3d::add_artcraft_3d;
use crate::configs::artcraft_website::add_artcraft_website;
@@ -48,6 +49,7 @@ fn do_build_cors_config(is_production: bool) -> Cors {
cors = add_artcraft_2d(cors, is_production);
cors = add_artcraft_3d(cors, is_production);
cors = add_artcraft_website(cors, is_production);
cors = add_artcraft_admin_dashboard(cors, is_production);
// Legacy
cors = add_legacy_trumped(cors, is_production);
@@ -0,0 +1,60 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use enums::common::job_status_plus::JobStatusPlus;
use errors::AnyhowResult;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
pub struct UserJobForModerationResult {
pub job_status: JobStatusPlus,
pub job_failure_reason: Option<String>,
pub credits_delta: Option<i32>,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
pub on_success_result_media_token: Option<MediaFileToken>,
pub job_token: InferenceJobToken,
pub wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub wallet_ledger_entry_type: Option<WalletLedgerEntryType>,
}
pub async fn list_user_jobs_for_moderation(
user_token: &UserToken,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Vec<UserJobForModerationResult>> {
let results = sqlx::query_as!(
UserJobForModerationResult,
r#"
SELECT
j.status as `job_status: enums::common::job_status_plus::JobStatusPlus`,
j.failure_reason as job_failure_reason,
wle.credits_delta,
wle.maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,
j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`
FROM generic_inference_jobs as j
LEFT OUTER JOIN users as u
ON j.maybe_creator_user_token = u.token
LEFT OUTER JOIN wallet_ledger_entries as wle
ON wle.maybe_entity_ref = j.token
WHERE u.token = ?
ORDER BY j.id DESC
"#,
user_token,
)
.fetch_all(mysql_pool)
.await;
match results {
Ok(records) => Ok(records),
Err(err) => {
warn!("list_user_jobs_for_moderation query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -5,6 +5,7 @@ pub mod get_pending_inference_job_count;
pub mod insert_generic_inference_job;
pub mod job_status;
pub mod kill_generic_inference_jobs;
pub mod list_user_jobs_for_moderation;
pub mod kill_jobs_in_development;
pub mod list_session_jobs;
pub mod mark_generic_inference_job_cancelled_by_user;
@@ -39,4 +39,5 @@ pub mod voice_conversion;
pub mod voice_designer;
pub mod w2l;
pub mod wallet_ledger_entries;
pub mod wallets;
pub mod wallets;
pub mod web_referrals;
@@ -20,6 +20,11 @@ pub struct CreateAccountFromEmailPasswordArgs<'a> {
pub ip_address: &'a str,
pub maybe_source: Option<UserSignupSource>,
/// The referral URL the user arrived from when signing up.
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
/// If the browser doesn't send this parameter, we'll try the `referer` header.
pub maybe_referral_url: Option<String>,
/// In production code, send this as `None`.
/// Only provide an external user token for db integration tests and db seeding tools.
/// This allows for knowing the user token a priori.
@@ -62,9 +67,11 @@ pub async fn create_account_from_email_and_password(
password_hash: args.password_hash,
is_without_password: false,
maybe_referral_url: args.maybe_referral_url,
// NB: This is just for testing.
maybe_user_token: args.maybe_user_token,
// These flags represent eagerly created users and are false for this flow
is_temporary: false,
email_is_synthetic: false,
@@ -57,6 +57,9 @@ pub async fn create_account_from_google_sso(
password_hash: SSO_PASSWORD,
is_without_password: true,
// TODO: Necessary? We're not doing SSO for now.
maybe_referral_url: None,
// NB: This is just for testing.
maybe_user_token: None,
@@ -26,6 +26,7 @@ pub struct CreateAccountFromStripeCheckoutArgs<'a> {
pub ip_address: &'a str,
pub maybe_source: Option<UserSignupSource>,
pub maybe_referral_url: Option<String>,
/// Comma separated string of feature flags.
pub maybe_feature_flags: Option<&'a str>,
@@ -68,6 +69,8 @@ pub async fn create_account_from_stripe_checkout(
password_hash: SSO_PASSWORD,
is_without_password: true,
maybe_referral_url: args.maybe_referral_url,
// NB: This is just for testing.
maybe_user_token: None,
},
@@ -60,6 +60,11 @@ pub struct GenericCreateAccountArgs<'a> {
/// Comma separated string of feature flags.
pub maybe_feature_flags: Option<&'a str>,
/// The referral URL the user arrived from when signing up.
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
/// If the browser doesn't send this parameter, we'll try the `referer` header.
pub maybe_referral_url: Option<String>,
/// In production code, send this as `None`.
/// Only provide an external user token for db integration tests and db seeding tools.
/// This allows for knowing the user token a priori.
@@ -124,7 +129,9 @@ SET
maybe_source = ?,
maybe_signup_method = ?
maybe_signup_method = ?,
maybe_referral_url = ?
"#,
user_token.as_str(),
@@ -159,6 +166,8 @@ SET
args.maybe_source.map(|s| s.to_str()),
args.maybe_signup_method.map(|m| m.to_str()),
args.maybe_referral_url,
);
@@ -0,0 +1,173 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::warn;
use sqlx::MySqlPool;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
pub struct LookupUserForModerationResult {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub username_is_generated: bool,
pub is_temporary: bool,
pub username_is_not_customized: bool,
pub email_address: String,
pub email_confirmed: bool,
pub email_is_synthetic: bool,
pub is_without_password: bool,
pub ip_address_creation: String,
pub ip_address_last_login: String,
pub maybe_avatar_media_file_token: Option<MediaFileToken>,
pub email_gravatar_hash: String,
pub is_banned: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn lookup_user_for_moderation_by_token(
token: &str,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<LookupUserForModerationResult>> {
let result = sqlx::query_as!(
LookupUserForModerationResult,
r#"
SELECT
users.token as `user_token: tokens::tokens::users::UserToken`,
username,
display_name,
username_is_generated as `username_is_generated: bool`,
is_temporary as `is_temporary: bool`,
username_is_not_customized as `username_is_not_customized: bool`,
email_address,
email_confirmed as `email_confirmed: bool`,
email_is_synthetic as `email_is_synthetic: bool`,
is_without_password as `is_without_password: bool`,
ip_address_creation,
ip_address_last_login,
maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,
email_gravatar_hash,
is_banned as `is_banned: bool`,
created_at,
updated_at
FROM users
WHERE
users.token = ?
AND users.user_deleted_at IS NULL
AND users.mod_deleted_at IS NULL
LIMIT 1
"#,
token,
)
.fetch_one(mysql_pool)
.await;
match result {
Ok(record) => Ok(Some(record)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(err) => {
warn!("lookup_user_for_moderation_by_token query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
pub async fn lookup_user_for_moderation_by_email(
email: &str,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<LookupUserForModerationResult>> {
let email = email.trim().to_lowercase();
let result = sqlx::query_as!(
LookupUserForModerationResult,
r#"
SELECT
users.token as `user_token: tokens::tokens::users::UserToken`,
username,
display_name,
username_is_generated as `username_is_generated: bool`,
is_temporary as `is_temporary: bool`,
username_is_not_customized as `username_is_not_customized: bool`,
email_address,
email_confirmed as `email_confirmed: bool`,
email_is_synthetic as `email_is_synthetic: bool`,
is_without_password as `is_without_password: bool`,
ip_address_creation,
ip_address_last_login,
maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,
email_gravatar_hash,
is_banned as `is_banned: bool`,
created_at,
updated_at
FROM users
WHERE
users.email_address = ?
AND users.user_deleted_at IS NULL
AND users.mod_deleted_at IS NULL
LIMIT 1
"#,
email,
)
.fetch_one(mysql_pool)
.await;
match result {
Ok(record) => Ok(Some(record)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(err) => {
warn!("lookup_user_for_moderation_by_email query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
pub async fn lookup_user_for_moderation_by_username(
username: &str,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<LookupUserForModerationResult>> {
let username = username.trim().to_lowercase();
let result = sqlx::query_as!(
LookupUserForModerationResult,
r#"
SELECT
users.token as `user_token: tokens::tokens::users::UserToken`,
username,
display_name,
username_is_generated as `username_is_generated: bool`,
is_temporary as `is_temporary: bool`,
username_is_not_customized as `username_is_not_customized: bool`,
email_address,
email_confirmed as `email_confirmed: bool`,
email_is_synthetic as `email_is_synthetic: bool`,
is_without_password as `is_without_password: bool`,
ip_address_creation,
ip_address_last_login,
maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,
email_gravatar_hash,
is_banned as `is_banned: bool`,
created_at,
updated_at
FROM users
WHERE
users.username = ?
AND users.user_deleted_at IS NULL
AND users.mod_deleted_at IS NULL
LIMIT 1
"#,
username,
)
.fetch_one(mysql_pool)
.await;
match result {
Ok(record) => Ok(Some(record)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(err) => {
warn!("lookup_user_for_moderation_by_username query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -3,3 +3,4 @@ pub mod lookup_user_for_login_by_email;
pub mod lookup_user_for_login_by_email_with_transactor;
pub mod lookup_user_for_login_by_username;
pub mod lookup_user_for_login_result;
pub mod lookup_user_for_moderation;
@@ -1,4 +1,4 @@
pub mod edit_user_profile_as_account_holder;
pub mod edit_user_profile_as_mod;
pub mod get_user_profile_by_token;
pub mod get_user_profile_by_username;
pub mod get_user_profile_by_username;
@@ -0,0 +1,53 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use enums::common::payments_namespace::PaymentsNamespace;
use errors::AnyhowResult;
use tokens::tokens::users::UserToken;
pub struct UserByStripeCustomerIdResult {
pub subscription_namespace: PaymentsNamespace,
pub maybe_stripe_subscription_id: Option<String>,
pub user_token: UserToken,
pub email_address: String,
pub username: String,
pub display_name: String,
}
pub async fn lookup_users_by_stripe_customer_id(
stripe_customer_id: &str,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Vec<UserByStripeCustomerIdResult>> {
let results = sqlx::query_as!(
UserByStripeCustomerIdResult,
r#"
SELECT
user_subscriptions.subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,
user_subscriptions.maybe_stripe_subscription_id,
users.token as `user_token: tokens::tokens::users::UserToken`,
users.email_address,
users.username,
users.display_name
FROM user_subscriptions
INNER JOIN users ON users.token = user_subscriptions.user_token
WHERE
user_subscriptions.maybe_stripe_customer_id = ?
AND user_subscriptions.deleted_at IS NULL
AND users.user_deleted_at IS NULL
AND users.mod_deleted_at IS NULL
ORDER BY user_subscriptions.created_at DESC
"#,
stripe_customer_id,
)
.fetch_all(mysql_pool)
.await;
match results {
Ok(records) => Ok(records),
Err(err) => {
warn!("lookup_users_by_stripe_customer_id query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -1,4 +1,5 @@
pub mod find_possibly_inactive_first_subscription_for_owner_user;
pub mod lookup_users_by_stripe_customer_id;
pub mod find_subscription_for_owner_user;
pub mod get_user_subscription_by_stripe_subscription_id;
pub mod list_active_user_subscriptions;
@@ -0,0 +1,60 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use chrono::{DateTime, Utc};
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use errors::AnyhowResult;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
pub struct WalletLedgerEntryForModerationResult {
pub token: WalletLedgerEntryToken,
pub entry_type: WalletLedgerEntryType,
pub maybe_entity_ref: Option<String>,
pub credits_delta: i32,
pub banked_credits_before: u32,
pub banked_credits_after: u32,
pub monthly_credits_before: u32,
pub monthly_credits_after: u32,
pub is_refunded: bool,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
pub created_at: DateTime<Utc>,
}
pub async fn get_wallet_ledger_entry_for_moderation(
wallet_ledger_entry_token: &WalletLedgerEntryToken,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<WalletLedgerEntryForModerationResult>> {
let result = sqlx::query_as!(
WalletLedgerEntryForModerationResult,
r#"
SELECT
token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,
maybe_entity_ref,
credits_delta,
banked_credits_before as `banked_credits_before: u32`,
banked_credits_after as `banked_credits_after: u32`,
monthly_credits_before as `monthly_credits_before: u32`,
monthly_credits_after as `monthly_credits_after: u32`,
is_refunded as `is_refunded: bool`,
maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
created_at as `created_at: DateTime<Utc>`
FROM wallet_ledger_entries
WHERE token = ?
LIMIT 1
"#,
wallet_ledger_entry_token,
)
.fetch_one(mysql_pool)
.await;
match result {
Ok(record) => Ok(Some(record)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(err) => {
warn!("get_wallet_ledger_entry_for_moderation query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -0,0 +1,64 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use chrono::{DateTime, Utc};
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use errors::AnyhowResult;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use tokens::tokens::wallets::WalletToken;
// TODO(bt): This will need to be paginated. Sort direction would be nice, too.
pub struct WalletLedgerEntryForModerationResult {
pub token: WalletLedgerEntryToken,
pub wallet_token: WalletToken,
pub entry_type: WalletLedgerEntryType,
pub maybe_entity_ref: Option<String>,
pub credits_delta: i32,
pub banked_credits_before: u32,
pub banked_credits_after: u32,
pub monthly_credits_before: u32,
pub monthly_credits_after: u32,
pub created_at: DateTime<Utc>,
pub is_refunded: bool,
pub maybe_linked_refund_ledger_token: Option<WalletLedgerEntryToken>,
}
pub async fn list_wallet_ledger_entries_by_wallet(
wallet_token: &WalletToken,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Vec<WalletLedgerEntryForModerationResult>> {
let results = sqlx::query_as!(
WalletLedgerEntryForModerationResult,
r#"
SELECT
token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,
entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,
maybe_entity_ref,
credits_delta,
banked_credits_before as `banked_credits_before: u32`,
banked_credits_after as `banked_credits_after: u32`,
monthly_credits_before as `monthly_credits_before: u32`,
monthly_credits_after as `monthly_credits_after: u32`,
created_at as `created_at: DateTime<Utc>`,
is_refunded as `is_refunded: bool`,
maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
FROM wallet_ledger_entries
WHERE wallet_token = ?
ORDER BY id DESC
"#,
wallet_token,
)
.fetch_all(mysql_pool)
.await;
match results {
Ok(records) => Ok(records),
Err(err) => {
warn!("list_wallet_ledger_entries_by_wallet query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -1,2 +1,4 @@
pub (crate) mod internal_insert_wallet_ledger_entry;
pub mod internal_insert_wallet_created_ledger_entry;
pub mod internal_insert_wallet_created_ledger_entry;
pub mod get_wallet_ledger_entry_for_moderation;
pub mod list_wallet_ledger_entries_by_wallet;
@@ -10,6 +10,7 @@ pub async fn add_durable_banked_balance_to_wallet(
wallet_token: &WalletToken,
amount_to_add: u64,
maybe_ledger_ref: Option<&str>,
wallet_ledger_entry_type_override: Option<WalletLedgerEntryType>,
transaction: &mut sqlx::Transaction<'_, MySql>,
) -> anyhow::Result<WalletUpdateSummary> {
@@ -38,7 +39,7 @@ pub async fn add_durable_banked_balance_to_wallet(
let record = InsertWalletLedgerEntry {
wallet_token,
entry_type: WalletLedgerEntryType::CreditBanked,
entry_type: wallet_ledger_entry_type_override.unwrap_or(WalletLedgerEntryType::CreditBanked),
maybe_entity_ref: maybe_ledger_ref.map(|t| t.to_string()),
credits_delta: amount_to_add.to_i64().unwrap_or(0),
@@ -54,7 +55,6 @@ pub async fn add_durable_banked_balance_to_wallet(
let wallet_ledger_entry_token = record.upsert_with_transaction(transaction).await?;
Ok(WalletUpdateSummary {
wallet_token: wallet.token,
wallet_ledger_entry_token,
@@ -0,0 +1,55 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use errors::AnyhowResult;
use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
pub struct WalletForModerationResult {
pub token: WalletToken,
pub wallet_namespace: PaymentsNamespace,
pub owner_user_token: UserToken,
pub banked_credits: u32,
pub monthly_credits: u32,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn get_wallet_for_moderation(
wallet_token: &WalletToken,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<WalletForModerationResult>> {
let result = sqlx::query_as!(
WalletForModerationResult,
r#"
SELECT
w.token as `token: tokens::tokens::wallets::WalletToken`,
w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,
w.owner_user_token as `owner_user_token: tokens::tokens::users::UserToken`,
w.banked_credits as `banked_credits: u32`,
w.monthly_credits as `monthly_credits: u32`,
w.version,
w.created_at as `created_at: DateTime<Utc>`,
w.updated_at as `updated_at: DateTime<Utc>`
FROM wallets as w
WHERE w.token = ?
LIMIT 1
"#,
wallet_token,
)
.fetch_one(mysql_pool)
.await;
match result {
Ok(record) => Ok(Some(record)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(err) => {
warn!("get_wallet_for_moderation query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -0,0 +1,52 @@
use anyhow::anyhow;
use log::warn;
use sqlx::MySqlPool;
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use errors::AnyhowResult;
use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
pub struct UserWalletForModerationResult {
pub token: WalletToken,
pub wallet_namespace: PaymentsNamespace,
pub banked_credits: u32,
pub monthly_credits: u32,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn list_user_wallets_for_moderation(
user_token: &UserToken,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Vec<UserWalletForModerationResult>> {
let results = sqlx::query_as!(
UserWalletForModerationResult,
r#"
SELECT
w.token as `token: tokens::tokens::wallets::WalletToken`,
w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,
w.banked_credits as `banked_credits: u32`,
w.monthly_credits as `monthly_credits: u32`,
w.version,
w.created_at as `created_at: DateTime<Utc>`,
w.updated_at as `updated_at: DateTime<Utc>`
FROM wallets as w
WHERE w.owner_user_token = ?
ORDER BY w.id DESC
"#,
user_token,
)
.fetch_all(mysql_pool)
.await;
match results {
Ok(records) => Ok(records),
Err(err) => {
warn!("list_user_wallets_for_moderation query error: {:?}", err);
Err(anyhow!("query error"))
}
}
}
@@ -6,4 +6,6 @@ pub mod find_primary_wallet_token_for_owner;
pub mod refill_monthly_credits_balance_on_wallet;
pub mod refund;
pub mod spend;
pub mod get_wallet_for_moderation;
pub mod list_user_wallets_for_moderation;
pub mod wallet_update_summary;
@@ -0,0 +1,41 @@
use sqlx::{MySql, Pool};
pub struct InsertWebReferralArgs<'a> {
pub url: &'a str,
pub maybe_domain: Option<&'a str>,
pub utm_source: Option<&'a str>,
pub utm_medium: Option<&'a str>,
pub utm_campaign: Option<&'a str>,
pub ip_address: &'a str,
pub maybe_anonymous_visitor_token: Option<&'a str>,
}
pub async fn insert_web_referral(
mysql_pool: &Pool<MySql>,
args: InsertWebReferralArgs<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO web_referrals (
url,
domain,
utm_source,
utm_medium,
utm_campaign,
ip_address,
anonymous_visitor_token
) VALUES (?, ?, ?, ?, ?, ?, ?)
"#,
args.url,
args.maybe_domain,
args.utm_source,
args.utm_medium,
args.utm_campaign,
args.ip_address,
args.maybe_anonymous_visitor_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
@@ -0,0 +1 @@
pub mod insert_web_referral;
@@ -41,6 +41,10 @@ pub enum WalletLedgerEntryType {
#[serde(rename = "refund_banked")]
RefundBanked,
/// Support staff manually credit account
#[serde(rename = "staff_add_banked")]
StaffAddBanked,
// TODO: No clean way to do "mixed" refunds yet, and if we
// refund close to the cutoff it might be unfair. Let's
// just not do monthly refunds yet and instead credit our
@@ -66,6 +70,7 @@ impl WalletLedgerEntryType {
Self::DeductBanked => "deduct_banked",
Self::DeductMonthly => "deduct_monthly",
Self::RefundBanked => "refund_banked",
Self::StaffAddBanked => "staff_add_banked",
}
}
@@ -78,6 +83,7 @@ impl WalletLedgerEntryType {
"deduct_banked" => Ok(Self::DeductBanked),
"deduct_monthly" => Ok(Self::DeductMonthly),
"refund_banked" => Ok(Self::RefundBanked),
"staff_add_banked" => Ok(Self::StaffAddBanked),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -93,6 +99,7 @@ impl WalletLedgerEntryType {
Self::DeductBanked,
Self::DeductMonthly,
Self::RefundBanked,
Self::StaffAddBanked,
])
}
}
@@ -114,6 +121,7 @@ mod tests {
assert_serialization(WalletLedgerEntryType::DeductBanked, "deduct_banked");
assert_serialization(WalletLedgerEntryType::DeductMonthly, "deduct_monthly");
assert_serialization(WalletLedgerEntryType::RefundBanked, "refund_banked");
assert_serialization(WalletLedgerEntryType::StaffAddBanked, "staff_add_banked");
}
}
@@ -129,6 +137,7 @@ mod tests {
assert_eq!(WalletLedgerEntryType::DeductBanked.to_str(), "deduct_banked");
assert_eq!(WalletLedgerEntryType::DeductMonthly.to_str(), "deduct_monthly");
assert_eq!(WalletLedgerEntryType::RefundBanked.to_str(), "refund_banked");
assert_eq!(WalletLedgerEntryType::StaffAddBanked.to_str(), "staff_add_banked");
}
#[test]
@@ -140,6 +149,7 @@ mod tests {
assert_eq!(WalletLedgerEntryType::from_str("deduct_banked").unwrap(), WalletLedgerEntryType::DeductBanked);
assert_eq!(WalletLedgerEntryType::from_str("deduct_monthly").unwrap(), WalletLedgerEntryType::DeductMonthly);
assert_eq!(WalletLedgerEntryType::from_str("refund_banked").unwrap(), WalletLedgerEntryType::RefundBanked);
assert_eq!(WalletLedgerEntryType::from_str("staff_add_banked").unwrap(), WalletLedgerEntryType::StaffAddBanked);
assert!(WalletLedgerEntryType::from_str("foo").is_err());
}
}
@@ -150,7 +160,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = WalletLedgerEntryType::all_variants();
assert_eq!(variants.len(), 7);
assert_eq!(variants.len(), 8);
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::Create));
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::CreditBanked));
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::CreditMonthly));
@@ -158,6 +168,7 @@ mod tests {
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::DeductBanked));
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::DeductMonthly));
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::RefundBanked));
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::StaffAddBanked));
assert_eq!(variants.pop_first(), None);
}
}
@@ -38,9 +38,10 @@ pub async fn complete_credits_pack_purchase(
info!("Adding {} credits to wallet: {}", credits_purchased, wallet_token.as_str());
let _result = add_durable_banked_balance_to_wallet(
&wallet_token,
credits_purchased,
&wallet_token,
credits_purchased,
maybe_ledger_ref,
None,
transaction,
).await?;
@@ -1,11 +1,8 @@
use crate::billing_action_fulfillment::artcraft_billing_action::SubscriptionPaidEvent;
use crate::billing_action_fulfillment::subscriptions::upsert_subscription_details::CrudType;
use enums::common::payments_namespace::PaymentsNamespace;
use log::{info, warn};
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id::get_user_subscription_by_stripe_subscription_id_transactional;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_with_invoice_paid_status_by_stripe_id::UpsertUserSubscriptionWithInvoicePaidStatus;
use mysql_queries::queries::wallets::add_durable_banked_balance_to_wallet::add_durable_banked_balance_to_wallet;
use mysql_queries::queries::wallets::create_new_artcraft_wallet_for_owner_user::create_new_artcraft_wallet_for_owner_user;
use mysql_queries::queries::wallets::find_primary_wallet_token_for_owner::find_primary_wallet_token_for_owner_using_transaction;
use mysql_queries::queries::wallets::refill_monthly_credits_balance_on_wallet::refill_monthly_credits_balance_on_wallet;
@@ -85,6 +85,15 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
CommonWebError::ServerError
})?;
let maybe_referral_url = request.maybe_referral_url.clone()
.or_else(|| {
http_request.headers().get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.filter(|s| !s.is_empty())
.map(|s| s.chars().take(255).collect::<String>());
let maybe_user_metadata = internal_user_lookup
.lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection)
.await
@@ -101,6 +110,7 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl
&price_id,
&mut mysql_connection,
&stripe_config,
maybe_referral_url,
).await?
},
Some(user_metadata) => {
@@ -28,6 +28,7 @@ pub (super) async fn user_creation_case(
price_id: &PriceId,
mysql_connection: &mut PoolConnection<MySql>,
stripe_config: &ArtcraftStripeConfigWithClient,
maybe_referral_url: Option<String>,
) -> Result<CreationPayload, CommonWebError> {
let mut transaction = mysql_connection.begin()
@@ -82,6 +83,7 @@ pub (super) async fn user_creation_case(
maybe_feature_flags: Some(&user_feature_flags),
ip_address: &ip_address,
maybe_source,
maybe_referral_url: maybe_referral_url.clone(),
},
Transactor::for_transaction(&mut transaction),
).await;
@@ -225,6 +225,65 @@ use tokens::tokens::user_bookmarks::*;
use tokens::tokens::users::*;
use tokens::tokens::zs_voice_datasets::*;
// Cost estimate
use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::*;
use artcraft_api_defs::generate::cost_estimate::estimate_video_cost::*;
// Image multi-function
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::*;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::*;
// Image edit (missing handlers)
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::*;
use artcraft_api_defs::generate::image::edit::gemini_25_flash_edit_image::*;
use artcraft_api_defs::generate::image::edit::qwen_edit_image::*;
use artcraft_api_defs::generate::image::edit::seededit_3_edit_image::*;
// Image inpaint
use artcraft_api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::*;
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::*;
// Video (missing handlers)
use artcraft_api_defs::generate::video::generate_seedance_1_0_pro_image_to_video::*;
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::*;
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::*;
// Video multi-function
use artcraft_api_defs::generate::video::multi_function::kling_2_5_turbo_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::kling_2_6_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::sora_2_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::sora_2_pro_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::veo_3p1_multi_function_video_gen::*;
use artcraft_api_defs::generate::video::multi_function::veo_3p1_fast_multi_function_video_gen::*;
// Object multi-function
use artcraft_api_defs::generate::object::multi_function::hunyuan3d_v3_multi_function_object_gen::*;
// Analytics, credits, subscriptions, media files
use artcraft_api_defs::analytics::log_active_user::*;
use artcraft_api_defs::credits::get_session_credits::*;
use artcraft_api_defs::subscriptions::get_session_subscription::*;
use artcraft_api_defs::media_file::list_batch_generated_media_files::*;
// Handler modules with locally-defined types
use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*;
use artcraft_api_defs::moderation::user::user_lookup::*;
use artcraft_api_defs::moderation::user::user_lookup_by_stripe_customer_id::*;
use artcraft_api_defs::moderation::jobs::user::list_user_jobs::*;
use artcraft_api_defs::moderation::wallet_ledger_entries::list_wallet_ledger_entries_by_wallet::*;
use artcraft_api_defs::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry::*;
use artcraft_api_defs::moderation::wallets::list_user_wallets::*;
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::*;
use artcraft_api_defs::moderation::wallets::moderator_get_wallet::*;
use artcraft_api_defs::web_referrals::log_web_referral::*;
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
use crate::http_server::endpoints::webhooks::fal_webhook_handler::*;
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*;
use crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::*;
use crate::http_server::endpoints::credits::get_session_credits_handler::*;
use crate::http_server::endpoints::subscriptions::get_session_subscription_handler::*;
use crate::http_server::endpoints::analytics::log_app_active_user_handler::*;
use crate::http_server::endpoints::analytics::log_app_active_user_json_handler::*;
use crate::http_server::endpoints::media_files::list::list_batch_generated_redux_media_files_handler::*;
#[derive(OpenApi)]
#[openapi(
paths(
@@ -341,6 +400,68 @@ use tokens::tokens::zs_voice_datasets::*;
crate::http_server::endpoints::weights::search::search_model_weights_http_post_handler::search_model_weights_http_post_handler,
crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::set_model_weight_cover_image_handler,
crate::http_server::endpoints::weights::update::update_weight_handler::update_weight_handler,
// Cost Estimate
crate::http_server::endpoints::generate::cost_estimate::image::estimate_image_cost_handler::estimate_image_cost_handler,
crate::http_server::endpoints::generate::cost_estimate::video::estimate_video_cost_handler::estimate_video_cost_handler,
// Generate Images (Multi-Function)
crate::http_server::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_handler::nano_banana_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_handler::nano_banana_pro_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_handler::nano_banana_2_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_handler::gpt_image_1p5_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_handler::bytedance_seedream_v4_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_handler::bytedance_seedream_v4p5_multi_function_image_gen_handler,
crate::http_server::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_handler::bytedance_seedream_5_lite_multi_function_image_gen_handler,
// Generate Images (Edit)
crate::http_server::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image_handler::flux_pro_kontext_max_edit_image_handler,
crate::http_server::endpoints::generate::image::edit::gemini_25_flash_edit_image_handler::gemini_25_flash_edit_image_handler,
crate::http_server::endpoints::generate::image::edit::qwen_edit_image_handler::qwen_edit_image_handler,
crate::http_server::endpoints::generate::image::edit::seededit_3_edit_image_handler::seededit_3_edit_image_handler,
// Generate Images (Inpaint)
crate::http_server::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_handler::flux_dev_juggernaut_inpaint_image_handler,
crate::http_server::endpoints::generate::image::inpaint::flux_pro_1_inpaint_handler::flux_pro_1_inpaint_image_handler,
// Generate Videos
crate::http_server::endpoints::generate::video::generate_seedance_1_0_pro_image_to_video_handler::generate_seedance_1_0_pro_image_to_video_handler,
crate::http_server::endpoints::generate::video::generate_veo_3_image_to_video_handler::generate_veo_3_image_to_video_handler,
crate::http_server::endpoints::generate::video::generate_veo_3_fast_image_to_video_handler::generate_veo_3_fast_image_to_video_handler,
// Generate Video (Multi-Function)
crate::http_server::endpoints::generate::video::multi_function::kling_2p5_turbo_pro_multi_function_video_gen_handler::kling_2p5_turbo_pro_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::kling_2p6_pro_multi_function_video_gen_handler::kling_2p6_pro_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen_handler::seedance_2p0_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::sora_2_multi_function_video_gen_handler::sora_2_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::sora_2_pro_multi_function_video_gen_handler::sora_2_pro_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::veo_3p1_multi_function_video_gen_handler::veo_3p1_multi_function_video_gen_handler,
crate::http_server::endpoints::generate::video::multi_function::veo_3p1_fast_multi_function_video_gen_handler::veo_3p1_fast_multi_function_video_gen_handler,
// Generate Objects (Multi-Function)
crate::http_server::endpoints::generate::object::multi_function::generate_hunyuan3d_v3_multi_function_object_handler::generate_hunyuan3d_v3_multi_function_object_handler,
// Media Files
crate::http_server::endpoints::media_files::list::list_batch_generated_redux_media_files_handler::list_batch_generated_redux_media_files_handler,
// Analytics
crate::http_server::endpoints::analytics::log_app_active_user_handler::log_app_active_user_handler,
crate::http_server::endpoints::analytics::log_app_active_user_json_handler::log_app_active_user_json_handler,
// Voice Conversion
crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::enqueue_infer_seed_vc_handler,
// TTS
crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler,
// Moderation
crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler,
crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler,
crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler,
crate::http_server::endpoints::moderation::jobs::user::list_user_jobs_handler::list_user_jobs_handler,
crate::http_server::endpoints::moderation::wallet_ledger_entries::list_wallet_ledger_entries_by_wallet_handler::list_wallet_ledger_entries_by_wallet_handler,
crate::http_server::endpoints::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry_handler::moderator_get_wallet_ledger_entry_handler,
crate::http_server::endpoints::moderation::wallets::list_user_wallets_handler::list_user_wallets_handler,
crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler,
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
// Credits
crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler,
// Subscriptions
crate::http_server::endpoints::subscriptions::get_session_subscription_handler::get_session_subscription_handler,
// Web Referrals
crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler,
// Webhooks
crate::http_server::endpoints::webhooks::fal_webhook_handler::fal_webhook_handler,
// Image Studio
crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::update_gpt_image_job_status_handler,
),
components(schemas(
// Tokens
@@ -510,6 +631,30 @@ use tokens::tokens::zs_voice_datasets::*;
EditUserFeatureFlagsError,
EditUserFeatureFlagsOption,
EditUserFeatureFlagsRequest,
ModeratorUserLookupRequest,
ModeratorUserLookupSuccessResponse,
ModeratorUserLookupUserDetails,
ModeratorUserLookupByStripeCustomerIdRequest,
ModeratorUserLookupByStripeCustomerIdResponse,
ModeratorUserLookupByStripeCustomerIdEntry,
ListUserJobsPathInfo,
ListUserJobsResponse,
ListUserJobsEntry,
ListUserWalletsPathInfo,
ListUserWalletsResponse,
ListUserWalletsEntry,
ListWalletLedgerEntriesByWalletPathInfo,
ListWalletLedgerEntriesByWalletResponse,
ListWalletLedgerEntriesByWalletEntry,
ModeratorAddBankedBalanceToWalletPathInfo,
ModeratorAddBankedBalanceToWalletRequest,
ModeratorAddBankedBalanceToWalletResponse,
ModeratorGetWalletPathInfo,
ModeratorGetWalletResponse,
ModeratorGetWalletDetails,
ModeratorGetWalletLedgerEntryPathInfo,
ModeratorGetWalletLedgerEntryResponse,
ModeratorGetWalletLedgerEntryDetails,
GptImage1EditImageRequest,
GptImage1EditImageImageSize,
GptImage1EditImageNumImages,
@@ -797,6 +942,115 @@ use tokens::tokens::zs_voice_datasets::*;
WriteEngineAssetMediaSuccessResponse,
WriteSceneFileMediaSuccessResponse,
ZsDatasetRecord,
// Cost Estimate types
EstimateImageCostRequest,
EstimateImageCostResponse,
EstimateImageCostError,
EstimateImageCostErrorType,
EstimateVideoCostRequest,
EstimateVideoCostResponse,
EstimateVideoCostError,
EstimateVideoCostErrorType,
// Image Multi-Function types
NanoBananaMultiFunctionImageGenRequest,
NanoBananaMultiFunctionImageGenResponse,
NanoBananaMultiFunctionImageGenAspectRatio,
NanoBananaMultiFunctionImageGenNumImages,
NanoBananaProMultiFunctionImageGenRequest,
NanoBananaProMultiFunctionImageGenResponse,
NanoBananaProMultiFunctionImageGenAspectRatio,
NanoBananaProMultiFunctionImageGenNumImages,
NanoBananaProMultiFunctionImageGenImageResolution,
NanaBanana2MultiFunctionImageGenRequest,
NanaBanana2MultiFunctionImageGenResponse,
NanaBanana2MultiFunctionImageGenAspectRatio,
NanaBanana2MultiFunctionImageGenNumImages,
NanaBanana2MultiFunctionImageGenImageResolution,
GptImage1p5MultiFunctionImageGenRequest,
GptImage1p5MultiFunctionImageGenResponse,
GptImage1p5MultiFunctionImageGenBackground,
GptImage1p5MultiFunctionImageGenInputFidelity,
GptImage1p5MultiFunctionImageGenNumImages,
GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize,
BytedanceSeedreamV4MultiFunctionImageGenRequest,
BytedanceSeedreamV4MultiFunctionImageGenResponse,
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
BytedanceSeedreamV4MultiFunctionImageGenNumImages,
BytedanceSeedreamV4p5MultiFunctionImageGenRequest,
BytedanceSeedreamV4p5MultiFunctionImageGenResponse,
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
BytedanceSeedream5LiteMultiFunctionImageGenRequest,
BytedanceSeedream5LiteMultiFunctionImageGenResponse,
BytedanceSeedream5LiteMultiFunctionImageGenImageSize,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
// Image Edit types
FluxProKontextMaxEditImageRequest,
FluxProKontextMaxEditImageResponse,
FluxProKontextMaxEditImageNumImages,
Gemini25FlashEditImageRequest,
Gemini25FlashEditImageResponse,
Gemini25FlashEditImageNumImages,
QwenEditImageRequest,
QwenEditImageResponse,
QwenEditImageAcceleration,
QwenEditImageNumImages,
SeedEdit3EditImageRequest,
SeedEdit3EditImageResponse,
// Image Inpaint types
FluxDevJuggernautInpaintImageRequest,
FluxDevJuggernautInpaintImageResponse,
FluxDevJuggernautInpaintImageNumImages,
FluxPro1InpaintImageRequest,
FluxPro1InpaintImageResponse,
FluxPro1InpaintImageNumImages,
// Video types (missing handlers)
GenerateSeedance10ProImageToVideoRequest,
GenerateSeedance10ProImageToVideoResponse,
GenerateVeo3ImageToVideoRequest,
GenerateVeo3ImageToVideoResponse,
GenerateVeo3FastImageToVideoRequest,
GenerateVeo3FastImageToVideoResponse,
// Video Multi-Function types
Kling2p5TurboProMultiFunctionVideoGenRequest,
Kling2p5TurboProMultiFunctionVideoGenResponse,
Kling2p6ProMultiFunctionVideoGenRequest,
Kling2p6ProMultiFunctionVideoGenResponse,
Seedance2p0MultiFunctionVideoGenRequest,
Seedance2p0MultiFunctionVideoGenResponse,
Sora2MultiFunctionVideoGenRequest,
Sora2MultiFunctionVideoGenResponse,
Sora2ProMultiFunctionVideoGenRequest,
Sora2ProMultiFunctionVideoGenResponse,
Veo3p1MultiFunctionVideoGenRequest,
Veo3p1MultiFunctionVideoGenResponse,
Veo3p1FastMultiFunctionVideoGenRequest,
Veo3p1FastMultiFunctionVideoGenResponse,
// Object Multi-Function types
Hunyuan3dV3MultiFunctionObjectGenRequest,
Hunyuan3dV3MultiFunctionObjectGenResponse,
// Other types
LogAppActiveUserRequest,
LogAppActiveUserResponse,
GetSessionCreditsPathInfo,
GetSessionCreditsResponse,
GetSessionSubscriptionPathInfo,
GetSessionSubscriptionResponse,
ListBatchGeneratedReduxMediaFilesPathInfo,
ListBatchGeneratedReduxMediaFilesSuccessResponse,
// Web Referrals
LogWebReferralRequest,
LogWebReferralResponse,
))
)]
pub struct ApiDoc;
@@ -31,7 +31,12 @@ impl AvtCookieManager {
}
pub fn make_new_cookie(&self) -> AnyhowResult<Cookie> {
let payload = AvtCookiePayload::new();
let token = AnonymousVisitorTrackingToken::generate();
self.make_new_cookie_with_apriori_token(&token)
}
pub fn make_new_cookie_with_apriori_token(&self, token: &AnonymousVisitorTrackingToken) -> AnyhowResult<Cookie> {
let payload = AvtCookiePayload::from_token(token.clone());
let claims = payload.to_map();
let jwt_string = self.jwt_signer.claims_to_jwt(&claims)?;
@@ -36,6 +36,13 @@ impl AvtCookiePayload {
}
}
pub fn from_token(avt_token: AnonymousVisitorTrackingToken) -> Self {
AvtCookiePayload {
avt_token,
cookie_version: COOKIE_VERSION,
}
}
pub fn from_map(map: BTreeMap<String, String>) -> Result<Self, InternalError> {
let avt_token = map.get("avt_token")
.ok_or_else(|| InternalError::VisitorCookieMissingField("avt_token"))?;
@@ -93,7 +93,7 @@ pub async fn create_beta_keys_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CreateBetaKeysError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::Implicit)
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => CreateBetaKeysError::ServerError,
@@ -91,7 +91,7 @@ pub async fn edit_beta_key_distributed_flag_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, EditBetaKeyDistributedFlagError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::Implicit)
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => EditBetaKeyDistributedFlagError::ServerError,
@@ -90,7 +90,7 @@ pub async fn edit_beta_key_note_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, EditBetaKeyNoteError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::Implicit)
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => EditBetaKeyNoteError::ServerError,
@@ -27,5 +27,6 @@ pub mod users;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod web_referrals;
pub mod webhooks;
pub mod weights;
@@ -68,6 +68,7 @@ impl fmt::Display for ModeratorTokenInfoError {
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/token_info/{token}",
responses(
(status = 200, description = "Found", body = ModeratorTokenInfoResponse),
@@ -84,7 +85,7 @@ pub async fn moderator_get_token_info_handler(
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ModeratorTokenInfoError> {
let user_session = require_moderator(&http_request, &server_state, UseDatabase::Implicit)
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => ModeratorTokenInfoError::ServerError,
@@ -1,3 +1,4 @@
pub mod get_tts_inference_queue_count;
pub mod get_w2l_inference_queue_count;
pub mod kill_tts_inference_jobs;
pub mod kill_tts_inference_jobs;
pub mod user;
@@ -0,0 +1,64 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::jobs::user::list_user_jobs::{
ListUserJobsEntry,
ListUserJobsPathInfo,
ListUserJobsResponse,
};
use mysql_queries::queries::generic_inference::web::list_user_jobs_for_moderation::list_user_jobs_for_moderation;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// List jobs for a user (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/jobs/user/{user_token}/list",
responses(
(status = 200, description = "Success", body = ListUserJobsResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("user_token" = UserToken, Path, description = "User token to look up jobs for"),
)
)]
pub async fn list_user_jobs_handler(
http_request: HttpRequest,
path: Path<ListUserJobsPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserJobsResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let results = list_user_jobs_for_moderation(&path.user_token, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("list_user_jobs error: {:?}", err);
CommonWebError::ServerError
})?;
let jobs = results.into_iter().map(|row| ListUserJobsEntry {
job_status: row.job_status,
job_failure_reason: row.job_failure_reason,
credits_delta: row.credits_delta,
maybe_linked_refund_ledger_token: row.maybe_linked_refund_ledger_token,
on_success_result_media_token: row.on_success_result_media_token,
job_token: row.job_token,
wallet_ledger_entry_token: row.wallet_ledger_entry_token,
wallet_ledger_entry_type: row.wallet_ledger_entry_type,
}).collect();
Ok(Json(ListUserJobsResponse {
success: true,
jobs,
}))
}
@@ -0,0 +1 @@
pub mod list_user_jobs_handler;
@@ -1,5 +1,8 @@
pub mod info;
pub mod ip_bans;
pub mod jobs;
pub mod user;
pub mod user_bans;
pub mod user_feature_flags;
pub mod user_feature_flags;
pub mod wallet_ledger_entries;
pub mod wallets;
@@ -0,0 +1,2 @@
pub mod moderator_user_lookup_by_stripe_customer_id_handler;
pub mod moderator_user_lookup_handler;
@@ -0,0 +1,69 @@
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::user::user_lookup_by_stripe_customer_id::{
ModeratorUserLookupByStripeCustomerIdEntry,
ModeratorUserLookupByStripeCustomerIdRequest,
ModeratorUserLookupByStripeCustomerIdResponse,
};
use mysql_queries::queries::users::user_subscriptions::lookup_users_by_stripe_customer_id::lookup_users_by_stripe_customer_id;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// Moderator User Lookup by Stripe Customer ID
#[utoipa::path(
post,
tag = "Moderation",
path = "/v1/moderation/users/lookup_by_stripe_customer_id",
request_body = ModeratorUserLookupByStripeCustomerIdRequest,
responses(
(status = 200, description = "Success", body = ModeratorUserLookupByStripeCustomerIdResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderator_user_lookup_by_stripe_customer_id_handler(
http_request: HttpRequest,
request: Json<ModeratorUserLookupByStripeCustomerIdRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorUserLookupByStripeCustomerIdResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let stripe_customer_id = request.stripe_customer_id.trim();
if stripe_customer_id.is_empty() {
return Ok(Json(ModeratorUserLookupByStripeCustomerIdResponse {
success: true,
users: vec![],
}));
}
let results = lookup_users_by_stripe_customer_id(stripe_customer_id, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("moderator_user_lookup_by_stripe_customer_id error: {:?}", err);
CommonWebError::ServerError
})?;
let users = results.into_iter().map(|row| ModeratorUserLookupByStripeCustomerIdEntry {
subscription_namespace: row.subscription_namespace,
maybe_stripe_subscription_id: row.maybe_stripe_subscription_id,
token: row.user_token,
email_address: row.email_address,
username: row.username,
display_name: row.display_name,
}).collect();
Ok(Json(ModeratorUserLookupByStripeCustomerIdResponse {
success: true,
users,
}))
}
@@ -0,0 +1,94 @@
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::user::user_lookup::{
ModeratorUserLookupRequest,
ModeratorUserLookupSuccessResponse,
ModeratorUserLookupUserDetails,
};
use mysql_queries::queries::users::user::get::lookup_user_for_moderation::{
lookup_user_for_moderation_by_email,
lookup_user_for_moderation_by_token,
lookup_user_for_moderation_by_username,
};
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
const LEGACY_USER_TOKEN_PREFIX: &str = "U:";
/// Moderator User Lookup
#[utoipa::path(
post,
tag = "Moderation",
path = "/v1/moderation/users/lookup",
request_body = ModeratorUserLookupRequest,
responses(
(status = 200, description = "Success", body = ModeratorUserLookupSuccessResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn moderator_user_lookup_handler(
http_request: HttpRequest,
request: Json<ModeratorUserLookupRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorUserLookupSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let search = request.search.trim();
if search.is_empty() {
return Ok(Json(ModeratorUserLookupSuccessResponse {
success: true,
maybe_user: None,
}));
}
let maybe_result = if search.starts_with(UserToken::token_prefix()) || search.starts_with(LEGACY_USER_TOKEN_PREFIX) {
lookup_user_for_moderation_by_token(search, &server_state.mysql_pool).await
} else if search.contains('@') {
lookup_user_for_moderation_by_email(search, &server_state.mysql_pool).await
} else {
lookup_user_for_moderation_by_username(search, &server_state.mysql_pool).await
};
let maybe_user = maybe_result
.map_err(|err| {
warn!("moderator_user_lookup error: {:?}", err);
CommonWebError::ServerError
})?;
let maybe_user_details = maybe_user.map(|user| ModeratorUserLookupUserDetails {
token: user.user_token,
username: user.username,
display_name: user.display_name,
username_is_generated: user.username_is_generated,
is_temporary: user.is_temporary,
username_is_not_customized: user.username_is_not_customized,
email_address: user.email_address,
email_confirmed: user.email_confirmed,
email_is_synthetic: user.email_is_synthetic,
is_without_password: user.is_without_password,
ip_address_creation: user.ip_address_creation,
ip_address_last_login: user.ip_address_last_login,
maybe_avatar_media_file_token: user.maybe_avatar_media_file_token,
email_gravatar_hash: user.email_gravatar_hash,
is_banned: user.is_banned,
created_at: user.created_at,
updated_at: user.updated_at,
});
Ok(Json(ModeratorUserLookupSuccessResponse {
success: true,
maybe_user: maybe_user_details,
}))
}
@@ -0,0 +1,68 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::wallet_ledger_entries::list_wallet_ledger_entries_by_wallet::{
ListWalletLedgerEntriesByWalletEntry,
ListWalletLedgerEntriesByWalletPathInfo,
ListWalletLedgerEntriesByWalletResponse,
};
use mysql_queries::queries::wallet_ledger_entries::list_wallet_ledger_entries_by_wallet::list_wallet_ledger_entries_by_wallet;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// List wallet ledger entries by wallet token (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/wallet_ledger_entries/wallet/{wallet_token}/list",
responses(
(status = 200, description = "Success", body = ListWalletLedgerEntriesByWalletResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("wallet_token" = WalletToken, Path, description = "Wallet token to look up ledger entries for"),
)
)]
pub async fn list_wallet_ledger_entries_by_wallet_handler(
http_request: HttpRequest,
path: Path<ListWalletLedgerEntriesByWalletPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListWalletLedgerEntriesByWalletResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let results = list_wallet_ledger_entries_by_wallet(&path.wallet_token, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("list_wallet_ledger_entries_by_wallet error: {:?}", err);
CommonWebError::ServerError
})?;
let entries = results.into_iter().map(|row| ListWalletLedgerEntriesByWalletEntry {
token: row.token,
wallet_token: row.wallet_token,
entry_type: row.entry_type,
maybe_entity_ref: row.maybe_entity_ref,
credits_delta: row.credits_delta,
banked_credits_before: row.banked_credits_before,
banked_credits_after: row.banked_credits_after,
monthly_credits_before: row.monthly_credits_before,
monthly_credits_after: row.monthly_credits_after,
created_at: row.created_at,
is_refunded: row.is_refunded,
maybe_linked_refund_ledger_token: row.maybe_linked_refund_ledger_token,
}).collect();
Ok(Json(ListWalletLedgerEntriesByWalletResponse {
success: true,
entries,
}))
}
@@ -0,0 +1,2 @@
pub mod list_wallet_ledger_entries_by_wallet_handler;
pub mod moderator_get_wallet_ledger_entry_handler;
@@ -0,0 +1,67 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry::{
ModeratorGetWalletLedgerEntryDetails,
ModeratorGetWalletLedgerEntryPathInfo,
ModeratorGetWalletLedgerEntryResponse,
};
use mysql_queries::queries::wallet_ledger_entries::get_wallet_ledger_entry_for_moderation::get_wallet_ledger_entry_for_moderation;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// Get a single wallet ledger entry by token (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/wallet_ledger_entry/{wallet_ledger_entry_token}",
responses(
(status = 200, description = "Success", body = ModeratorGetWalletLedgerEntryResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("wallet_ledger_entry_token" = WalletLedgerEntryToken, Path, description = "Wallet ledger entry token to look up"),
)
)]
pub async fn moderator_get_wallet_ledger_entry_handler(
http_request: HttpRequest,
path: Path<ModeratorGetWalletLedgerEntryPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorGetWalletLedgerEntryResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let maybe_entry = get_wallet_ledger_entry_for_moderation(&path.wallet_ledger_entry_token, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("moderator_get_wallet_ledger_entry error: {:?}", err);
CommonWebError::ServerError
})?;
let maybe_entry_details = maybe_entry.map(|entry| ModeratorGetWalletLedgerEntryDetails {
token: entry.token,
entry_type: entry.entry_type,
maybe_entity_ref: entry.maybe_entity_ref,
credits_delta: entry.credits_delta,
banked_credits_before: entry.banked_credits_before,
banked_credits_after: entry.banked_credits_after,
monthly_credits_before: entry.monthly_credits_before,
monthly_credits_after: entry.monthly_credits_after,
is_refunded: entry.is_refunded,
maybe_linked_refund_ledger_token: entry.maybe_linked_refund_ledger_token,
created_at: entry.created_at,
});
Ok(Json(ModeratorGetWalletLedgerEntryResponse {
success: true,
maybe_entry: maybe_entry_details,
}))
}
@@ -0,0 +1,63 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::wallets::list_user_wallets::{
ListUserWalletsEntry,
ListUserWalletsPathInfo,
ListUserWalletsResponse,
};
use mysql_queries::queries::wallets::list_user_wallets_for_moderation::list_user_wallets_for_moderation;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// List wallets for a user (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/wallets/user/{user_token}/list",
responses(
(status = 200, description = "Success", body = ListUserWalletsResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("user_token" = UserToken, Path, description = "User token to look up wallets for"),
)
)]
pub async fn list_user_wallets_handler(
http_request: HttpRequest,
path: Path<ListUserWalletsPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserWalletsResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let results = list_user_wallets_for_moderation(&path.user_token, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("list_user_wallets error: {:?}", err);
CommonWebError::ServerError
})?;
let wallets = results.into_iter().map(|row| ListUserWalletsEntry {
token: row.token,
wallet_namespace: row.wallet_namespace,
banked_credits: row.banked_credits,
monthly_credits: row.monthly_credits,
version: row.version,
created_at: row.created_at,
updated_at: row.updated_at,
}).collect();
Ok(Json(ListUserWalletsResponse {
success: true,
wallets,
}))
}
@@ -0,0 +1,3 @@
pub mod list_user_wallets_handler;
pub mod moderator_add_banked_balance_to_wallet_handler;
pub mod moderator_get_wallet_handler;
@@ -0,0 +1,75 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::{
ModeratorAddBankedBalanceToWalletPathInfo,
ModeratorAddBankedBalanceToWalletRequest,
ModeratorAddBankedBalanceToWalletResponse,
};
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
use mysql_queries::queries::wallets::add_durable_banked_balance_to_wallet::add_durable_banked_balance_to_wallet;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// Add banked credits to a wallet (moderation)
#[utoipa::path(
post,
tag = "Moderation",
path = "/v1/moderation/wallet/{wallet_token}/add_banked_balance",
request_body = ModeratorAddBankedBalanceToWalletRequest,
responses(
(status = 200, description = "Success", body = ModeratorAddBankedBalanceToWalletResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("wallet_token" = WalletToken, Path, description = "Wallet token to add credits to"),
)
)]
pub async fn moderator_add_banked_balance_to_wallet_handler(
http_request: HttpRequest,
path: Path<ModeratorAddBankedBalanceToWalletPathInfo>,
request: Json<ModeratorAddBankedBalanceToWalletRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let mut transaction = server_state.mysql_pool.begin()
.await
.map_err(|err| {
warn!("moderator_add_banked_balance_to_wallet transaction begin error: {:?}", err);
CommonWebError::ServerError
})?;
let _result = add_durable_banked_balance_to_wallet(
&path.wallet_token,
request.credits as u64,
None,
Some(WalletLedgerEntryType::StaffAddBanked),
&mut transaction,
)
.await
.map_err(|err| {
warn!("moderator_add_banked_balance_to_wallet error: {:?}", err);
CommonWebError::ServerError
})?;
transaction.commit()
.await
.map_err(|err| {
warn!("moderator_add_banked_balance_to_wallet commit error: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(ModeratorAddBankedBalanceToWalletResponse {
success: true,
}))
}
@@ -0,0 +1,64 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::wallets::moderator_get_wallet::{
ModeratorGetWalletDetails,
ModeratorGetWalletPathInfo,
ModeratorGetWalletResponse,
};
use mysql_queries::queries::wallets::get_wallet_for_moderation::get_wallet_for_moderation;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// Get a single wallet by token (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/wallet/{wallet_token}",
responses(
(status = 200, description = "Success", body = ModeratorGetWalletResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("wallet_token" = WalletToken, Path, description = "Wallet token to look up"),
)
)]
pub async fn moderator_get_wallet_handler(
http_request: HttpRequest,
path: Path<ModeratorGetWalletPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorGetWalletResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let maybe_wallet = get_wallet_for_moderation(&path.wallet_token, &server_state.mysql_pool)
.await
.map_err(|err| {
warn!("moderator_get_wallet error: {:?}", err);
CommonWebError::ServerError
})?;
let maybe_wallet_details = maybe_wallet.map(|wallet| ModeratorGetWalletDetails {
token: wallet.token,
wallet_namespace: wallet.wallet_namespace,
owner_user_token: wallet.owner_user_token,
banked_credits: wallet.banked_credits,
monthly_credits: wallet.monthly_credits,
version: wallet.version,
created_at: wallet.created_at,
updated_at: wallet.updated_at,
});
Ok(Json(ModeratorGetWalletResponse {
success: true,
maybe_wallet: maybe_wallet_details,
}))
}
@@ -41,6 +41,11 @@ pub struct CreateAccountRequest {
/// Optional: Source of the signup, e.g. "artcraft", "fakeyou", "storyteller", etc.
/// If not provided, we try to infer it from the Origin header instead.
pub signup_source: Option<UserSignupSource>,
/// Optional: The referral URL the user arrived from when signing up.
/// The browser can send `document.referrer` to the backend so we know how people are finding us.
/// If the browser doesn't send this parameter, we'll try the `referer` header.
pub maybe_referral_url: Option<String>,
}
#[derive(ToSchema, Serialize)]
@@ -195,6 +200,15 @@ pub async fn create_account_handler(
maybe_source = get_request_signup_source_enum(&http_request);
}
let maybe_referral_url = request.maybe_referral_url.clone()
.or_else(|| {
http_request.headers().get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.filter(|s| !s.is_empty())
.map(|s| s.chars().take(255).collect::<String>());
let create_account_result = create_account_from_email_and_password(
&mysql_pool,
CreateAccountFromEmailPasswordArgs {
@@ -205,6 +219,7 @@ pub async fn create_account_handler(
password_hash: &password_hash,
ip_address: &ip_address,
maybe_source,
maybe_referral_url,
maybe_user_token: None, // NB: This parameter is for internal testing only
}
).await;
@@ -0,0 +1,152 @@
use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{info, warn};
use url::Url;
use artcraft_api_defs::web_referrals::log_web_referral::{LogWebReferralRequest, LogWebReferralResponse};
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::web_referrals::insert_web_referral::{insert_web_referral, InsertWebReferralArgs};
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
/// Record a web referral for analytics tracking.
#[utoipa::path(
post,
tag = "Web Referrals",
path = "/v1/web_referrals/record",
responses(
(status = 200, description = "Success", body = LogWebReferralResponse),
(status = 500, description = "Server error"),
),
)]
pub async fn log_web_referral_handler(
http_request: HttpRequest,
request: web::Json<LogWebReferralRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError> {
// 1. Try to get the referral URL from the request body, falling back to the Referer header.
let referral_url = request.maybe_referral_url.as_deref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.or_else(|| {
http_request.headers().get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.filter(|s| !s.is_empty());
// 2. If no URL from either source, early exit 200 OK.
let referral_url = match referral_url {
Some(url) => url,
None => {
return ok_response();
}
};
// 3. Parse the URL (infallible — we don't fail on bad URLs).
// NB: Parse before truncating so we don't lose UTM params.
let parsed = Url::parse(&referral_url).ok();
// Truncate the URL to 255 characters for storage.
let referral_url: String = referral_url.chars().take(255).collect();
// 4. Extract domain if URL parsed.
let maybe_domain = parsed.as_ref()
.and_then(|u| u.host_str())
.map(|s| s.to_string());
// 5. Extract UTM parameters if URL parsed.
let utm_source = extract_query_param(parsed.as_ref(), "utm_source");
let utm_medium = extract_query_param(parsed.as_ref(), "utm_medium");
let utm_campaign = extract_query_param(parsed.as_ref(), "utm_campaign");
// 6. Grab the request IP address.
let ip_address = get_request_ip(&http_request);
// 7. Check inbound cookies for AVT. If not present, mint a new one.
let existing_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let (avt_token, maybe_new_avt_cookie) = match existing_avt_token {
Some(token) => (token, None),
None => {
let token = AnonymousVisitorTrackingToken::generate();
let cookie = server_state
.avt_cookie_manager
.make_new_cookie_with_apriori_token(&token)
.map_err(|e| {
warn!("AVT cookie creation error: {:?}", e);
CommonWebError::ServerError
})?;
(token, Some(cookie))
}
};
let avt_token_str = avt_token.to_string();
// Truncate UTM fields to fit the schema.
let utm_source = utm_source.map(|s| s.chars().take(32).collect::<String>());
let utm_medium = utm_medium.map(|s| s.chars().take(32).collect::<String>());
let utm_campaign = utm_campaign.map(|s| s.chars().take(255).collect::<String>());
// Insert into the database.
insert_web_referral(&server_state.mysql_pool, InsertWebReferralArgs {
url: &referral_url,
maybe_domain: maybe_domain.as_deref(),
utm_source: utm_source.as_deref(),
utm_medium: utm_medium.as_deref(),
utm_campaign: utm_campaign.as_deref(),
ip_address: &ip_address,
maybe_anonymous_visitor_token: Some(&avt_token_str),
}).await.map_err(|e| {
warn!("Error inserting web referral: {:?}", e);
CommonWebError::ServerError
})?;
info!("Recorded web referral: {}", referral_url);
// Build response, setting the AVT cookie if we minted a new one.
let response = LogWebReferralResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|_| CommonWebError::ServerError)?;
let mut response_builder = HttpResponse::Ok();
if let Some(cookie) = maybe_new_avt_cookie {
response_builder.cookie(cookie);
}
Ok(response_builder
.content_type("application/json")
.body(body))
}
fn ok_response() -> Result<HttpResponse, CommonWebError> {
let response = LogWebReferralResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|_| CommonWebError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
fn extract_query_param(parsed: Option<&Url>, key: &str) -> Option<String> {
parsed.and_then(|u| {
u.query_pairs()
.find(|(k, _)| k == key)
.map(|(_, v)| v.to_string())
})
}
@@ -0,0 +1 @@
pub mod log_web_referral_handler;
@@ -14,6 +14,7 @@ use crate::http_server::routes::application_routes::user_bookmarks_routes::add_u
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
use crate::http_server::routes::application_routes::user_routes::add_user_routes;
use crate::http_server::routes::application_routes::wallet_routes::add_wallet_routes;
use crate::http_server::routes::application_routes::web_referrals_routes::add_web_referrals_routes;
use crate::http_server::routes::application_routes::webhook_routes::add_webhook_routes;
use crate::http_server::routes::application_routes::weights_routes::add_weights_routes;
use actix_http::body::MessageBody;
@@ -55,6 +56,7 @@ where
// Other useful tools
app = add_analytics_routes(app); // /v1/analytics/...
app = add_web_referrals_routes(app); // /v1/web_referrals/...
// User and user-adjacent routes
app = add_comments_routes(app); // /v1/comments/...
@@ -14,6 +14,7 @@ mod user_bookmarks_routes;
mod user_rating_routes;
mod user_routes;
mod wallet_routes;
mod web_referrals_routes;
mod webhook_routes;
mod weights_routes;
pub (super) mod add_application_routes;
@@ -19,11 +19,19 @@ use crate::http_server::endpoints::moderation::ip_bans::add_ip_ban::add_ip_ban_h
use crate::http_server::endpoints::moderation::ip_bans::delete_ip_ban::delete_ip_ban_handler;
use crate::http_server::endpoints::moderation::ip_bans::get_ip_ban::get_ip_ban_handler;
use crate::http_server::endpoints::moderation::ip_bans::list_ip_bans::list_ip_bans_handler;
use crate::http_server::endpoints::moderation::jobs::user::list_user_jobs_handler::list_user_jobs_handler;
use crate::http_server::endpoints::moderation::wallet_ledger_entries::list_wallet_ledger_entries_by_wallet_handler::list_wallet_ledger_entries_by_wallet_handler;
use crate::http_server::endpoints::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry_handler::moderator_get_wallet_ledger_entry_handler;
use crate::http_server::endpoints::moderation::wallets::list_user_wallets_handler::list_user_wallets_handler;
use crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler;
use crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler;
use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler;
use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler;
use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler;
use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler;
use crate::http_server::endpoints::moderation::user_bans::list_banned_users::list_banned_users_handler;
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler;
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler;
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler;
pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
@@ -42,7 +50,48 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
.route(web::post().to(edit_user_feature_flags_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::scope("/users")
.service(web::resource("/lookup")
.route(web::post().to(moderator_user_lookup_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/lookup_by_stripe_customer_id")
.route(web::post().to(moderator_user_lookup_by_stripe_customer_id_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/jobs")
.service(web::resource("/user/{user_token}/list")
.route(web::get().to(list_user_jobs_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::resource("/wallet/{wallet_token}")
.route(web::get().to(moderator_get_wallet_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/wallet/{wallet_token}/add_banked_balance")
.route(web::post().to(moderator_add_banked_balance_to_wallet_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::scope("/wallets")
.service(web::resource("/user/{user_token}/list")
.route(web::get().to(list_user_wallets_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::resource("/wallet_ledger_entry/{wallet_ledger_entry_token}")
.route(web::get().to(moderator_get_wallet_ledger_entry_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::scope("/wallet_ledger_entries")
.service(web::resource("/wallet/{wallet_token}/list")
.route(web::get().to(list_wallet_ledger_entries_by_wallet_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
)
// NB: Old paths are not under /v1 -
.service(web::scope("/moderation")
.service(web::resource("/staff")
.route(web::get().to(list_staff_handler))
@@ -0,0 +1,26 @@
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::error::Error;
use actix_web::{web, App, HttpResponse};
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler;
pub fn add_web_referrals_routes<T, B> (app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
app.service(web::scope("/v1/web_referrals")
.service(web::resource("/record")
.route(web::post().to(log_web_referral_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
@@ -28,7 +28,7 @@ impl Display for RequireModeratorError {
impl Error for RequireModeratorError {}
pub enum UseDatabase<'a> {
Implicit,
GrabNewConnection,
FromPool(&'a mut PoolConnection<MySql>),
}
@@ -41,7 +41,7 @@ pub async fn require_moderator(
let mut saved_connection = None;
let mysql_connection = match database {
UseDatabase::Implicit => {
UseDatabase::GrabNewConnection => {
let mut connection = server_state.mysql_pool
.acquire()
.await
@@ -30,9 +30,11 @@ import {
getProviderDisplayName,
getProviderIconByName,
} from "@storyteller/model-list";
import { Viewer3D } from "@storyteller/ui-viewer-3d";
import Seo from "../../components/seo";
const VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"];
const MODEL_3D_EXTENSIONS = [".glb", ".gltf", ".fbx", ".spz"];
const COPY_FEEDBACK_DURATION = 1500;
const SHARE_URL_BASE = "https://getartcraft.com/media/";
@@ -41,6 +43,11 @@ const isVideoUrl = (url: string): boolean => {
return VIDEO_EXTENSIONS.some((ext) => urlLower.includes(ext));
};
const is3DModelUrl = (url: string): boolean => {
const urlLower = url.toLowerCase();
return MODEL_3D_EXTENSIONS.some((ext) => urlLower.includes(ext));
};
interface ContextImage {
media_links: { cdn_url: string; maybe_thumbnail_template: string };
media_token: string;
@@ -52,6 +59,7 @@ interface MediaData {
token: string | null;
createdAt: string | null;
isVideo: boolean;
is3D: boolean;
isLoaded: boolean;
width?: number;
height?: number;
@@ -140,6 +148,7 @@ export default function MediaPage() {
token: null,
createdAt: null,
isVideo: false,
is3D: false,
isLoaded: false,
creator: null,
});
@@ -170,6 +179,7 @@ export default function MediaPage() {
token: file.token || id,
createdAt: file.created_at || null,
isVideo: url ? isVideoUrl(url) : false,
is3D: url ? is3DModelUrl(url) : false,
isLoaded: isSameUrl ? prev.isLoaded : false,
width: isSameUrl ? prev.width : undefined,
height: isSameUrl ? prev.height : undefined,
@@ -200,6 +210,7 @@ export default function MediaPage() {
token: null,
createdAt: null,
isVideo: false,
is3D: false,
isLoaded: false,
creator: null,
});
@@ -211,6 +222,7 @@ export default function MediaPage() {
token: null,
createdAt: null,
isVideo: false,
is3D: false,
isLoaded: false,
creator: null,
});
@@ -266,7 +278,13 @@ export default function MediaPage() {
</div>
) : media.url ? (
<div className="relative h-full w-full flex items-center justify-center">
{media.isVideo ? (
{media.is3D ? (
<Viewer3D
modelUrl={addCorsParam(media.url) || media.url}
isActive
className="h-full w-full"
/>
) : media.isVideo ? (
<video
src={addCorsParam(media.url) || media.url}
className="h-full w-full object-contain"
@@ -353,7 +353,7 @@ export function Viewer3D({
return (
<div
className={`relative h-full w-full overflow-hidden rounded-xl bg-[#282828] ${className}`}
className={`relative h-full w-full overflow-hidden rounded-l-xl bg-[#282828] ${className}`}
onMouseDown={stopPropagation}
onMouseMove={stopPropagation}
onMouseUp={stopPropagation}